继承Java集合接口(Set,Map,List等)的C ++等价物是什么?

What is the C++ equivalent of inheriting a Java collection interface (Set, Map, List etc.)? Or extending AbstractCollection?

我已经开始用C++编写代码,来自Java背景(实际上我在我的大学学习过C++,但是我们从来没有到STL等)。

不管怎样,我已经到了在各种各样的集合中排列数据的地步,我马上告诉自己:"好的,这是一个集合;这是一个列表,或者是一个数组,这是一张地图等等。"在爪哇,我只需要写出我所写的任何一个类来实现集合或地图或列表界面;但是我可能不会走得更远。eriting arraylist或hashset或者其他什么,这里的实现有点复杂,我不想把它们搞得一团糟。

现在,我在C++中做什么(使用标准库)?对于集合、映射、列表等,似乎没有抽象的基类——相当于Java接口;另一方面,标准容器的实现看起来相当可怕。好吧,也许他们一知道你就不那么可怕了,但是假设我只是想写一个非抽象类的东西,用C++来扩展抽象集?我可以传递给任何需要集合的函数吗?我该怎么做呢?

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


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

没必要为此争论,事情就是这样。如果你不喜欢这个,用另一种语言。

答案是:有一个等价物,但它会让你有点不高兴,因为Java的容器和算法模型在很大程度上是基于继承的,但是C++的模型不是。C++的模型是基于泛型迭代器的。

例如,举个例子,您希望实现一个集合。忽略C++已经具有EDCOX1,0,EDCOX1,1,EDCOX1,2,EDCX1,3,以及这些都是可定制的不同的比较器和分配器,当然,无序的有可定制的散列函数。

所以假设你想重新实现std::set。也许你是个计算机科学的学生,你想比较一下AVL树,2-3棵树,红黑树和八字树。

你会怎么做?你会写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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);

当然,你不必写所有这些,特别是如果你只是写一些东西来测试它们的一部分。但是如果你写了所有这些(为了清晰起见,我排除了一点),那么你将拥有一个功能完备的集合类。那套课有什么特别之处呢?

你可以在任何地方使用它。任何与std::set配合使用的设备都可以与您的设备配合使用。它不需要专门为它编程。它不需要任何东西。在任何集合类型上工作的任何东西都应该在它上工作。Boost的任何算法都可以在集合上工作。

你写的任何在集合上使用的算法都会在你的集合上工作,并且提升集合和许多其他集合。但不仅仅是在片场上。如果它们写得很好,那么它们将在任何支持特定类型迭代器的容器上工作。如果他们需要随机访问,他们需要随机访问迭代器,这是std::vector提供的,但std::list不提供的。如果他们需要双向访问迭代器,那么std::vectorstd::list和其他(和其他)可以正常工作,但std::forward_list不会。

迭代器/算法/容器的工作非常好。考虑在C++中读取文件到字符串中的整洁性:

1
2
3
4
5
using namespace std;

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


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

注意:C++不是Java。不要尝试用C++编程Java。如果你想编程Java,程序Java:它比在C++中尝试这样做要好得多。如果你想编程C++,程序C++。


你需要尝试放弃Java思维方式。你看,STL的好处在于它通过迭代器将算法与容器分离。

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

以下是所有容器:http://en.cppreference.com/w/cpp/container

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

您可能希望继承的原因有两个:

  • 您希望重用实现(坏主意)
  • 通过使行为可用(例如从抽象集之类的基类继承)重用现有算法

要简单地触摸第一个点,如果需要存储一组对象(比如游戏场景中的一组对象),请确实这样做,将这些对象的数组作为场景对象的成员。不需要子类来充分利用容器。换句话说,比起继承,更喜欢组合。这已经完成了,并且在Java世界中被接受为做"正确的事情"。看这里的讨论,它在GOF的书中!同样的事情也适用于C++。

例子:

为了解决第二点,让我们考虑一个场景。你在做一个二维的Sidecroller游戏,你有一个Scene对象,有一个GameObject的数组。这些GameObjects有位置,你想按位置对它们排序,并做二进制搜索来找到最近的对象,例如。

在C++思想中,元素的存储和容器的操作是两个不同的事情。容器类为创建/插入/删除提供了最低限度的功能。上面有意思的东西都归算法。它们之间的桥梁是迭代器。这个想法是,你是否使用EDCOX1×3?(相当于Java的ARARYLIST,我想),或者你自己的实现是无关的,只要对元素的访问是相同的。下面是一个人为的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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返回的迭代器捕获了这个。因此,我们可以排序并执行二进制搜索。

The essence of the vector is random access to elements

我们可以将向量换成任何其他随机访问结构,而不需要继承,代码仍然工作得很好:

1
2
3
4
5
6
7
8
9
10
11
12
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};
}

有关参考,请参阅我使用的函数:

  • STD::排序
  • STD::下限

为什么这是继承的有效选择?

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

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


它不被称为STL)有许多现有的容器类型:EDOCX1,0,EDCOX1,2,EDCX1,3,EDCX1,4,EDCX1,6,EDCX1,7,EDCX1,9,EDCX1,10,EDCX1,11,EDOCX1,EDOCX1 C++标准库(注:13)、queuepriority_queue。机会是,你只想直接使用其中的一个——你肯定不想从中得到。但是,在某一点上,您当然可能需要实现自己的特殊容器类型,如果它与某个接口匹配,那就更好了,对吧?

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

当然,除了容器之外,还有许多其他的要求。例如,该标准为不同类型的迭代器、随机数生成器等提供了需求。

ISO C++委员会的许多人(实际上是研究小组8)正在考虑使这些概念成为语言的一个特征。建议将允许您为需要满足的类型指定需求,以便将它们用作模板类型参数。例如,您可以编写这样一个模板函数:

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

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


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