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,以及这些都是可定制的不同的比较器和分配器,当然,无序的有可定制的散列函数。
所以假设你想重新实现
你会怎么做?你会写:
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); |
当然,你不必写所有这些,特别是如果你只是写一些东西来测试它们的一部分。但是如果你写了所有这些(为了清晰起见,我排除了一点),那么你将拥有一个功能完备的集合类。那套课有什么特别之处呢?
你可以在任何地方使用它。任何与
你写的任何在集合上使用的算法都会在你的集合上工作,并且提升集合和许多其他集合。但不仅仅是在片场上。如果它们写得很好,那么它们将在任何支持特定类型迭代器的容器上工作。如果他们需要随机访问,他们需要随机访问迭代器,这是
迭代器/算法/容器的工作非常好。考虑在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游戏,你有一个
在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}; } |
这里需要注意的是,我使用
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)、
但是,没有,容器派生的一些抽象基类。然而,C++标准为类型(有时称为概念)提供了要求。例如,如果您查看C++ 11标准(或此处)的第23.2节,您将发现容器的要求。例如,所有容器都必须有一个默认的构造函数,该构造函数在恒定时间内创建一个空容器。然后对顺序容器(如
当然,除了容器之外,还有许多其他的要求。例如,该标准为不同类型的迭代器、随机数生成器等提供了需求。
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这样的语言愉快地运行。