【问题标题】:What is the C++ equivalent of inheriting a Java collection interface (Set, Map, List etc.)? Or extending AbstractCollection?继承 Java 集合接口(Set、Map、List 等)的 C++ 等价物是什么?还是扩展 AbstractCollection?
【发布时间】:2014-01-16 18:24:54
【问题描述】:

我已经开始使用 C++ 编码,来自 Java 背景(实际上我在大学学习过 C++,但我们从未学习过 STL 等)

不管怎样,我已经到了在各种集合中排列数据的地步,我立即告诉自己“好的,这是一种 Set;这是一个 List,或者一个 ArrayList;这是一张地图等。”在 Java 中,我会简单地让我正在编写的任何类实现 Set 或 Map 或 List 接口;但我可能不会继承 ArrayList 或 HashSet 之类的东西,那里的实现有点涉及,我不想把它们搞砸。

现在,我在 C++(使用标准库)中做什么? Sets、Maps、Lists 等似乎没有抽象基类——相当于 Java 接口;另一方面,标准容器的实现看起来很可怕。好吧,也许一旦你了解了它们,它们就不会那么可怕了,但是假设我只是想写一些像在 C++ 中扩展 AbstractSet 的非虚拟类之类的东西?我可以传递给任何需要 Set 的函数吗?我该怎么做呢?

澄清一下 - 我不一定想做 Java 中的常见做法。但是,另一方面,如果我有一个对象,从概念上讲,它是一种集合,我想继承适当的东西,免费获得默认实现,并在我的 IDE 的指导下实现我应该实现的那些方法。

【问题讨论】:

  • 一般来说,继承不是 C++(或至少是 C++-标准库)实现可重用性的方式。泛型编程(使用模板实现)是。例如,用于迭代集合的函数不采用抽象类参数,它是迭代器类型的模板。
  • 我确实是。通用的编程方法是要么隐含这些关系(并且冒着不太理想的错误消息的风险,至少在语言标准的未来修订之前),或者采用类型级元编程技巧,导致代码像if(is_iterator<T>::value) { /* do something with iterators */ } .
  • 该解释是正确的,以“这些天”为模。泛型编程并不是什么新鲜事,标准库从标准的第一个版本开始就接受了它,甚至早于标准。不要感到震惊,很高兴你能够学习一种新的编程范式;-) OOP 并不是万能的,它只是众多有用工具中的一种。
  • 这对你来说可能会很有趣:stackoverflow.com/questions/1039853/…
  • @einpoklum 是的,这是回避公认约定的缺点 :-) 还有很多其他库提供各种集合(两个流行的集合:boost 和 Qt),但我不知道是否临时它们中的任何一个都是基于继承的。至于期望 SL 容器的代码:如果它们接受迭代器,那么您几乎可以采用任何容器,无论多么反常,非常容易:编写一个适当的迭代器类。

标签: java c++ inheritance stl containers


【解决方案1】:

简短的回答是:没有等价物,因为 C++ 做事不同。

没有必要争论这个,事情就是这样。如果您不喜欢这样,请使用其他语言。

长答案是:有一个等价物,但它会让你有点不开心,因为虽然 Java 的容器和算法模型主要基于继承,但 C++ 不是。 C++ 的模型主要基于泛型迭代器。

举个例子,假设你想实现一个集合。忽略 C++ 已经有 std::setstd::multisetstd::unordered_setstd::unordered_multiset 的事实,这些都可以使用不同的比较器和分配器进行定制,而无序的则具有可定制的哈希函数,当然。

假设你想重新实现std::set。例如,也许您是一名计算机科学专业的学生,​​并且想要比较 AVL 树、2-3 树、红黑树和展开树。

你会怎么做?你会写:

template<class Key, class Compare = std::less<Key>, class Allocator = std::allocator<Key>> 
class set {
    using key_type = Key;
    using value_type = Key;
    using size_type = std::size_t;
    using difference_type = std::ptrdiff_t;
    using key_compare = Compare;
    using value_compare = Compare;
    using allocator_type = Allocator;
    using reference = value_type&;
    using const_reference = const value_type&;
    using pointer = std::allocator_traits<Allocator>::pointer;
    using const_pointer = std::allocator_traits<Allocator>::const_pointer;
    using iterator = /* depends on your implementation */;
    using const_iterator = /* depends on your implementation */;
    using reverse_iterator = std::reverse_iterator<iterator>;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>

    iterator begin() const;
    iterator end() const;
    const_iterator cbegin() const;
    const_iterator cend() const;
    reverse_iterator rbegin() const;
    reverse_iterator rend() const;
    const_reverse_iterator crbegin() const;
    const_reverse_iterator crend() const;

    bool empty() const;
    size_type size() const;
    size_type max_size() const;

    void clear();

    std::pair<iterator, bool> insert(const value_type& value);
    std::pair<iterator, bool> insert(value_type&& value);
    iterator insert(const_iterator hint, const value_type& value);
    iterator insert(const_iterator hint, value_type&& value);
    template <typename InputIterator>
    void insert(InputIterator first, InputIterator last);
    void insert(std::initializer_list<value_type> ilist);

    template <class ...Args>
    std::pair<iterator, bool> emplace(Args&&... args);

    void erase(iterator pos);
    iterator erase(const_iterator pos);
    void erase(iterator first, iterator last);
    iterator erase(const_iterator first, const_iterator last);
    size_type erase(const key_type& key);

    void swap(set& other);

    size_type count(const Key& key) const;
    iterator find(const Key& key);
    const_iterator find(const Key& key) const;

    std::pair<iterator, iterator> equal_range(const Key& key);
    std::pair<const_iterator, const_iterator> equal_range(const Key& key) const;

    iterator lower_bound(const Key& key);
    const_iterator lower_bound(const Key& key) const;
    iterator upper_bound(const Key& key);
    const_iterator upper_bound(const Key& key) const;

    key_compare key_comp() const;
    value_compare value_comp() const;
}; // offtopic: don't forget the ; if you've come from Java!

template<class Key, class Compare, class Alloc>
void swap(set<Key,Compare,Alloc>& lhs, 
          set<Key,Compare,Alloc>& rhs);

template <class Key, class Compare, class Alloc>
bool operator==(const set<Key,Compare,Alloc>& lhs,
                const set<Key,Compare,Alloc>& rhs);

template <class Key, class Compare, class Alloc>
bool operator!=(const set<Key,Compare,Alloc>& lhs,
                const set<Key,Compare,Alloc>& rhs);

template <class Key, class Compare, class Alloc>
bool operator<(const set<Key,Compare,Alloc>& lhs,
               const set<Key,Compare,Alloc>& rhs);

template <class Key, class Compare, class Alloc>
bool operator<=(const set<Key,Compare,Alloc>& lhs,
                const set<Key,Compare,Alloc>& rhs);

template <class Key, class Compare, class Alloc>
bool operator>(const set<Key,Compare,Alloc>& lhs,
               const set<Key,Compare,Alloc>& rhs);

template <class Key, class Compare, class Alloc>
bool operator>=(const set<Key,Compare,Alloc>& lhs,
                const set<Key,Compare,Alloc>& rhs);

当然,您不必编写所有这些,特别是如果您只是编写一些东西来测试它们的一部分。但是如果你写了所有这些(为了清楚起见,我排除了更多),那么你将拥有一个功能齐全的集合类。那个 set 类有什么特别之处?

您可以在任何地方使用它。任何适用于std::set 的东西都适用于您的设置。它不必专门为它编程。它不需要任何东西。任何适用于任何集合类型的东西都应该适用于它。 Boost 的任何算法都适用于集合。

您编写的用于集合的任何算法都将适用于您的集合、boost 的集合以及许多其他集合。但不仅仅是在片场。如果它们写得很好,它们将适用于任何支持特定类型迭代器的容器。如果他们需要随机访问,他们将需要 RandomAccessIterators,std::vector 提供,但 std::list 没有。如果他们需要双向迭代器,那么 std::vectorstd::list(以及其他)可以正常工作,但 std::forward_list 不会。

迭代器/算法/容器的东西真的很好用。考虑在 C++ 中将文件读入字符串的简洁性:

using namespace std;

ifstream file("file.txt");
string file_contents(istreambuf_iterator<char>(file),
                     istreambuf_iterator<char>{});

【讨论】:

  • 所以,你的意思是——至少在标准库中,并且在代码中具有相同的精神——我从不编写带有集合指针或集合引用的函数,我只是编写接受任何类的模板函数,并将其视为一个集合,这样如果所有方法都在那里,它会起作用吗?
  • 面向对象与继承无关。请,请,请意识到这一点。面向对象的代码是面向对象的代码。上面的代码是面向对象的。动态多态/继承/基于类的OO只是一种形式。请不要将“Java OO”与一般的 OO 混淆。
  • Bjarne 肯定不会帮助你,因为他和所有其他有能力的 C++ 程序员一样,鄙视人们尝试用 C++ 编写 Java/Python/C/其他语言的方式。如果你想写 Java,写 JAVA。如果你想写 C++,写 C++。
  • 为什么要写一个带集合的函数?
  • 我相信这个帖子变得不必要的尖刻和适得其反。
【解决方案2】:

标准 C++ 库已经实现了列表、映射、集合等。C++ 中没有必要再次实现这些数据结构。如果您实现类似这些数据结构之一的东西,您将实现相同的概念(即,使用相同的函数名称、参数顺序、嵌套类型的名称等)。容器有多种概念(序列、关联容器等)。更重要的是,您将使用适当的迭代器概念公开结构的内容。

注意:C++ 不是 Java。不要尝试用 C++ 编写 Java。如果您想编写 Java,请编写 Java:它比尝试在 C++ 中编写要好得多。如果你想编写 C++,就编写 C++。

【讨论】:

  • 但是,可能有各种各样的列表、地图、集合等,它们的行为与默认值不同;并且对于插入和删除有特殊意义的人——也许是替代的,也许是默认的附加。那么我如何表明我的班级“是一个集合”?我怎样才能将它作为一个集合在代码中“传递”呢?
  • @einpoklum:是的,这就是为什么您实现相同的概念并让您的功能传递我们的自定义实体以根据这些概念实现。在实践中,我发现用完全任意的容器来实现功能很少有用,除了能够查看对象序列,这些对象序列是根据迭代器巧妙地完成的,当然,这些迭代器也只是概念(即,没有迭代器基类,至少没有一个单独有用的)。
  • @einpoklum 您通过相应的命名和记录表明它是一个集合。您无需继承任何东西即可将其传递给采用集合的代码:如果该代码确实采用集合概念(而不是集合具体类),则它在集合类型中被模板化。跨度>
  • @delnan:我不明白你的最后评论。您认为您可以将其扩展为答案吗? “集合类型中的模板化”是什么意思?你的意思是只有&lt;set&gt;中的代码可以说是采取sets-the-concept吗?
  • @einpoklum 不。接受集合的函数看起来不像:void foo(AbstractSet&lt;int&gt; s) { ... },因为没有 AbstractSet 这样的东西。相反,它看起来像这样:template &lt;typename SetType&gt; void foo(SetType s)。这与&lt;set&gt; 没有任何关系,恰恰相反。
【解决方案3】:

您需要尝试摆脱 Java 思维方式。你看,STL 的美妙之处在于它通过迭代器将算法与容器分开。

长话短说:将迭代器传递给您的算法。不要继承。

这里是所有容器:http://en.cppreference.com/w/cpp/container

以下是所有算法:http://en.cppreference.com/w/cpp/algorithm

您可能想要继承的原因可能有两个:

  • 您想重用实现(坏主意)
  • 通过使行为可用来重用现有算法(例如,从像 AbstractSet 这样的基类继承)

简要介绍第一点,如果您需要存储一个数组(比如游戏场景中的一个对象数组),就这样做,将这些对象的数组作为 Scene 对象的成员。无需子类化即可充分利用容器。换句话说,比继承更喜欢组合。这已经被做死了,并且在 Java 世界中被接受为做“正确的事情”。 See discussion here,在 GoF 书中!同样的道理也适用于 C++。

示例:

为了解决第二点,让我们考虑一个场景。您正在制作一个 2D 横向卷轴游戏,并且您有一个 Scene 对象,其中包含一个 GameObjects 数组。这些GameObjects 有位置,你想按位置对它们进行排序,然后进行二进制搜索以找到最近的对象,例如。

在 C++ 思维模式中,元素的存储和容器的操作是两个独立的事情。容器类提供了最基本的功能,用于创建/插入/删除。以上任何有趣的东西都归入算法。而它们之间的桥梁是迭代器。这个想法是,无论您使用std::vector&lt;GameObject&gt;(我认为相当于Java 的ArrayList),还是您自己的实现都是无关紧要的,只要对元素的访问是相同的。这是一个人为的例子:

struct GameObject {
    float x, y;

    // compare just by x position
    operator < (GameObject const& other)
    {
        return x < other.x;
    }
};

void example() {
    std::vector<GameObject> objects = {
        GameObject{8, 2},
        GameObject{4, 3},
        GameObject{6, 1}
    };
    std::sort(std::begin(objects), std::end(objects));
    auto nearestObject = std::lower_bound(std::begin(objects), std::end(objects), GameObject{5, 12});

    // nearestObject should be pointing to GameObject{4,3};
}

这里需要注意的是,我使用std::vector 来存储我的对象这一事实与我可以对其元素执行随机访问这一事实无关紧要。 vector 返回的迭代器捕获了这一点。这样我们就可以排序并执行二分查找了。

向量的本质是随机访问元素

我们可以将向量替换为任何其他随机访问结构,无需继承,代码仍然可以正常工作:

void example() {
    // using a raw array this time.
    GameObject objects[] = {
        GameObject{8, 2},
        GameObject{4, 3},
        GameObject{6, 1}
    };
    std::sort(std::begin(objects), std::end(objects));
    auto nearestObject = std::lower_bound(std::begin(objects), std::end(objects), GameObject{5, 12});

    // nearestObject should be pointing to GameObject{4,3};
}

参考,看我用过的函数:

为什么这是继承的有效替代方法?

这种方法为可扩展性提供了两个正交方向:

  • 无需继承即可添加新容器,只需提供迭代器访问即可。 所有现有算法都有效
  • 可以添加新算法。 所有支持这些迭代器的容器都可以使用这些新算法,过去、现在或未来。

【讨论】:

  • 请原谅我争论的语气,但是 - 当然,你在开玩笑!你是在暗示我知道我的集合是一个集合!这怎么可能?
  • @einpoklum:我不认为亚历山大是在开玩笑!要意识到的关键是,除了继承之外还有其他形式的多态性,而泛型编程是另一种植根于概念的形式。
  • 什么是“面向对象的代码”?人们在没有真正考虑的情况下抛出这个术语。对他们来说,这通常意味着“一切都是一个对象,并且有很多继承”。它应该是模块化、封装、关注点分离等。继承只是一个工具。仅仅因为你有一个继承锤,问题不应该都是钉子。
  • @einpoklum:正如我所说:C++ 不是 Java。你会做不同的事情。这并不意味着您需要对所有内容都使用模板。但是,您将使用适合您的问题的自定义界面,而不是不适合您的问题的容器界面。顺便说一句,我认为泛型编程是面向对象的——只是不使用动态多态(是的,我意识到大多数人认为动态多态是面向对象的核心)。
  • @einpoklum 没关系。如果你不觉得它是面向对象的,它不会阻止它以不同的方式解决问题(在我看来,它更加模块化)。如果您习惯了 OOP,并且不愿意仅仅因为您感到不舒服而寻找不同的解决问题的方法,那么情况就不同了。也许您应该花一些时间来了解 C++ 方法为您提供了什么,而不是放弃它并退回到用 C++ 编写 Java。
【解决方案4】:

C++ 标准库(注意:它不称为 STL)有许多现有的容器类型:vectorarraydequeforward_listlistsetmapmultisetmultimapunordered_setunordered_mapunordered_multisetunordered_multimapstackqueuepriority_queue。很有可能,您只想直接使用其中之一 - 您当然从不想从它们中派生。但是,您肯定有可能在某些时候需要实现自己的特殊容器类型,并且如果它匹配某个接口会很好,对吧?

但不,容器没有派生自一些抽象基类。然而,C++ 标准为类型(有时称为概念)提供了要求。例如,如果您查看 C++11 标准(或 here)的第 23.2 节,您会发现容器的要求。例如,所有容器都必须有一个默认构造函数,该构造函数在恒定时间内创建一个空容器。然后对Sequence Containers(如std::vector)和Associative Containers(如std::map)有更具体的要求。您可以对您的类进行编码以满足这些要求,然后人们就可以按照他们的期望安全地使用您的容器。

当然,除了容器之外,还有很多其他方面的要求。例如,标准对不同类型的迭代器、随机数生成器等提出了要求。


ISO C++ 委员会(实际上是第 8 研究组)中的一些人正在研究使这些概念成为该语言的一个特性。该提案将允许您指定需要满足的类型的要求,才能将它们用作模板类型参数。例如,您可以像这样编写一个模板函数:

template <Sequence_container C>
void foo(C container); // This will only accept sequence containers
// or even just:
void foo(Sequence_container container);

但是,我认为这超出了您对 C++ 的理解范围。

【讨论】:

  • 标准容器——set、vector、map 等——是满足这些要求的最低兼容类吗?如果是,继承它们是否合理?
  • @einpoklum 不,继承它们是不合理的。它们是成熟的容器。您应该使用它们,而不是从它们继承,就像您不会在 Java 中继承 ArrayList
  • @einpoklum 除了 sftrabbit 的观点之外,它们甚至不是概念的最低兼容实现。例如,std::vector 具有容量,而概念没有提及。
  • 那么我如何“表达”我的容器是一个顺序容器的事实?表达概念,如果你愿意?我很困惑...
  • @Einpoklum 您确保您的容器符合要求,并在文档中声明它是一个顺序容器。
【解决方案5】:

在 C++ 中,集合(也称为容器)和对其进行操作的通用算法是以完全不知道继承的方式实现的。相反,连接它们的是迭代器:对于每个容器,指定它提供的迭代器类别,对于每个算法,说明它使用的迭代器类别。所以在某种程度上,迭代器将其他两个“桥接”在一起,这就是 STL 如何将容器和算法的数量保持在最低限度(N+M 而不是 N*M)。容器进一步定义为序列容器(vector、deque、list(双链表)或 forward_list(单链表)和关联容器(map、set、hashmap、hashset 等)。序列容器与性能有关(即一个是针对不同情况的更好选择)。关联容器关注事物如何存储在其中及其结果(二叉树与散列数组)。类似的想法适用于算法。这是 STL 示例的通用编程的要点通过专门且有意地不面向对象。实际上,您将不得不扭曲纯 OO 方法以实现流畅的泛型编程。这样的范式不能与 Java 或 Smalltalk 等语言一起愉快地使用

【讨论】:

    猜你喜欢
    • 2012-02-26
    • 2016-03-17
    • 1970-01-01
    • 2011-05-25
    • 2019-07-17
    • 2017-08-04
    • 2020-05-12
    • 2011-10-13
    • 1970-01-01
    相关资源
    最近更新 更多