繁体   English   中英

继承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 ++进行编码,来自Java背景(实际上我在我的大学学过C ++,但是我们从未进入过STL等)

无论如何,我已经到了我在各种集合中安排数据的地步,我立即告诉自己“好吧,这是一种Set;这是一个List或一个ArrayList;这是地图等“ 在Java中,我只想让我正在编写的任何类实现Set或Map或List接口; 但我可能不会继续继承ArrayList或HashSet,或者什么不是,那里的实现有一些参与,我不想搞砸它们。

现在,我在C ++中做什么(使用标准库)? 似乎没有集合,映射,列表等的抽象基类 - 相当于Java接口; 另一方面,标准容器的实现看起来非常可怕。 好吧,也许他们一旦你了解它们就不那么可怕了,但是假设我只是想写一些类似于在C ++中扩展AbstractSet的非虚拟类? 我可以传递给任何需要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一起使用的东西都可以使用你的set。 它不必专门为它编程。 它不需要任何东西。 任何适用于任何集合类型的东西都应该适用于它。 Boost的任何算法都可以在集合上运行。

你编写的用于集合的任何算法都可以在你的集合和boost集合以及许多其他集合上运行。 但不只是集合。 如果它们被正确编写,它们将在任何支持特定类型迭代器的容器上工作。 如果他们需要随机访问,他们将需要RandomAccessIterators, std::vector提供,但std::list不需要。 如果他们需要BidirectionalIterators,那么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 sidescroller游戏,你有一个Scene对象,带有一个GameObject数组。 这些GameObjects有位置,你想按位置对它们进行排序,并做二进制搜索以找到最近的对象,作为一个例子。

在C ++思维模式中,元素的存储和容器的操作是两个不同的东西。 容器类提供最小的功能,用于创建/插入/删除。 以上任何有趣的内容都将归入Algorithms。 它们之间的桥梁是迭代器 我的想法是你是否使用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};
}

供参考,请参阅我使用的功能:

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

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

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

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

但不,没有容器派生的一些抽象基类。 但是,C ++标准提供了对类型的要求 (有时称为概念 )。 例如,如果您查看C ++ 11标准(或此处 )的第23.2节,您将找到Container的要求。 例如,所有容器必须具有默认构造函数,该构造函数在常量时间内创建空容器。 然后对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 ++的理解。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM