继承Java集合接口(Set、Map、List等)的C++等价物是什么?或者扩展AbstractCollection

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

本文关键字:等价物 C++ 是什么 AbstractCollection 扩展 或者 接口 集合 Java Set List      更新时间:2023-10-16

我从Java背景开始用C++编程(实际上我在大学学习过C++,但我们从未学习过STL等)

无论如何,我已经到了将数据排列在各种集合中的地步,我立即告诉自己:"好吧,这是一种集合;这是一个列表,或ArrayList;这是映射等。"在Java中,我只需要让我正在编写的任何类实现Set、map或List接口;但我可能不会继承ArrayList或HashSet,或者其他什么,那里的实现有点涉及,我不想把它们搞砸。

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

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

简单的答案是:没有等价物,因为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);

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

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

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

迭代器/算法/容器的功能非常好。考虑在C++中将文件读取为字符串的清洁度:

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

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

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

要简单地谈到第一点,如果您需要存储一组东西(比如游戏场景中的一组对象),请将这些对象的数组作为scene对象的成员。没有必要通过子类来充分利用容器。换句话说,更喜欢组合而不是继承。这已经被做得死死的了,在Java世界中被认为是在做"正确的事情"。见这里的讨论,它在GoF的书中!同样的道理也适用于C++。

示例:

为了解决第二点,让我们考虑一个场景。你正在制作一个2D杂耍游戏,你有一个Scene对象,有一个由GameObjects组成的数组。例如,这些GameObjects有位置,你想按位置对它们进行排序,并进行二进制搜索以找到最近的对象。

在C++思想中,元素的存储和容器的操作是两件独立的事情。容器类为创建/插入/删除提供了最低限度的功能。以上任何有趣的东西都归为算法。它们之间的桥梁是迭代器。其思想是,无论您使用std::vector<GameObject>(我认为相当于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};
}

作为参考,请参阅我使用过的功能:

  • std::排序
  • std::下限

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

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

  • 只需提供迭代器访问,就可以添加新的容器,而无需继承所有现有算法都有效
  • 可以添加新的算法所有支持这些迭代器的容器都将使用这些新算法,无论是过去的、现在的还是将来的

C++标准库(注意:它不称为STL)有许多现有的容器类型:vectorarraydequeforward_listlistsetmapmultisetmultimapunordered_setunordered_mapunordered_multisetunordered_multimapstackqueuepriority_queue。很可能,你只想直接使用其中一个——你肯定永远不想从中派生。然而,在某个时刻,您可能需要实现自己的特殊容器类型,如果它匹配一些接口,那就太好了,对吧?

但是不,容器并没有从中派生出一些抽象基类。然而,C++标准为类型(有时称为概念)提供了要求。例如,如果您查看C++11标准的第23.2节(或此处),您会发现容器的要求。例如,所有容器都必须有一个默认构造函数,该构造函数在恒定时间内创建一个空容器。然后对序列容器(如std::vector)和关联容器(如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++的理解。

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