Что такое эквивалент С++ для наследования интерфейса коллекции Java (Set, Map, List и т.д.)? Или расширение AbstractCollection?

Я начал кодирование на С++, исходя из фона Java (фактически, я изучил С++ в своем университете, но мы никогда не попадали в STL и т.д.)

В любом случае, я дошел до того, что собираю данные во всех коллекциях, и сразу говорю себе: "Хорошо, это своего рода набор, и это List или ArrayList; и это карта и т.д." В Java я просто хотел бы, чтобы любой класс, который я пишу, реализовал интерфейс Set или Map или List; но я бы, вероятно, не дошел до того, что наследовал ArrayList или HashSet, а что - нет, реализации там были вовлечены, и я бы не хотел их испортить.

Теперь, что мне делать в С++ (со стандартной библиотекой)? Кажется, что нет абстрактных базовых классов для Sets, Maps, Lists и т.д. - эквивалент интерфейсов Java; с другой стороны, реализации стандартных контейнеров выглядят довольно ужасно. Хорошо, может быть, они не так ужасны, как только вы их узнаете, но предположим, что я просто хотел написать что-то вроде не виртуального класса, расширяющего AbstractSet в С++? Что-то я мог бы передать его любой функции, которая принимает Set? Как мне это сделать?

Просто для уточнения - я не обязательно хочу делать то, что распространяет на Java. Но, с другой стороны, если у меня есть объект, который концептуально является своего рода набором, я хочу наследовать что-то подходящее, получить бесплатные решения по умолчанию и руководствоваться моей IDE для реализации тех методов, которые я должен реализовать.

Ответ 1

Короткий ответ: нет эквивалента, потому что С++ делает вещи по-другому.

Нет смысла спорить об этом, это как раз то, что происходит. Если вам это не нравится, используйте другой язык.

Долгий ответ: есть эквивалент, но это сделает вас немного недовольными, потому что, хотя Java-модель контейнеров и алгоритмов сильно основана на наследовании, С++ - нет. Модель С++ в значительной степени основана на общих итераторах.

Скажем, возьмем ваш пример, что вы хотите реализовать набор. Игнорируя тот факт, что С++ уже имеет std::set, std::multiset, std::unordered_set и std::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 и множестве других наборов. Но не только на множествах. Если они написаны грамотно, они будут работать на любом контейнере, поддерживающем конкретный тип итератора. Если им нужен произвольный доступ, им потребуются RandomAccessIterators, которые std::vector предоставляет, но std::list нет. Если им нужны двунаправленныеИтераторы, то std::vector и std::list (и другие) будут работать нормально, но std::forward_list не будет.

Работа итератора/алгоритма/контейнера работает очень хорошо. Рассмотрим чистоту чтения файла в строке в С++:

using namespace std;

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

Ответ 2

Стандартная библиотека С++ уже реализует списки, карты, наборы и т.д. На С++ нет смысла снова внедрять эти структуры данных. Если вы реализуете что-то вроде одной из этих структур данных, вы должны реализовать ту же концепцию (например, использовать те же имена функций, порядок параметров, имена вложенных типов и т.д.). Существуют различные концепции для контейнера (последовательность, ассоциативные контейнеры и т.д.). Что еще более важно, вы бы разоблачили содержимое своей структуры, используя соответствующие концепции итератора.

Примечание. С++ не является Java. Не пытайтесь программировать Java на С++. Если вы хотите запрограммировать Java, программируйте Java: она работает намного лучше, чем пытаться сделать это на С++. Если вы хотите запрограммировать С++, запрограммируйте С++.

Ответ 3

Вам нужно попробовать и отпустить мышление Java. Понимаете, красота STL заключается в том, что он отделяет алгоритмы от контейнеров через итераторы.

Короче говоря: пройдите по итераторам к вашим алгоритмам. Не наследуйте.

Вот все контейнеры: http://en.cppreference.com/w/cpp/container

И вот все алгоритмы: http://en.cppreference.com/w/cpp/algorithm

Могут быть две причины, по которым вы можете наследовать:

  • Вы хотите повторно использовать реализацию (плохая идея)
  • Повторно использовать существующие алгоритмы, создавая доступное поведение (например, наследуя от базового класса, такого как AbstractSet)

Чтобы кратко коснуться первой точки, если вам нужно сохранить массив вещей (например, массив объектов в игровой сцене), сделайте именно это, имея массив этих объектов в качестве члена объекта Scene. Нет необходимости в подклассе, чтобы полностью использовать контейнер. Другими словами, предпочитайте композицию над наследованием. Это уже сделано до смерти и принято в мире Java, как "Правая вещь". Смотрите обсуждение здесь, это в книге GoF! То же самое относится к С++.

Пример:

Чтобы обратиться ко второму пункту, рассмотрим сценарий. Вы делаете 2D-прокрутку, и у вас есть объект Scene с массивом GameObject s. Эти GameObjects имеют позиции, и вы хотите отсортировать их по положению и выполнить двоичный поиск, чтобы найти ближайший объект в качестве примера.

В менталитете С++ хранение элементов и манипулирование контейнерами - это две разные вещи. Классы контейнеров обеспечивают минимальную функциональность для создания/вставки/удаления. Все, что интересно выше, относится к алгоритмам. И мост между ними - итераторы. Идея заключается в том, используете ли вы 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 capture. В результате мы можем сортировать и выполнять двоичный поиск.

Суть вектора - случайный доступ к элементам

Мы можем поменять вектор для любой другой структуры произвольного доступа без наследования, а код все еще отлично работает:

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};
}

Для справки см. функции, которые я использовал:

Почему это допустимая альтернатива наследованию?

Этот подход дает два ортогональных направления растяжимости:

  • Новые контейнеры могут быть добавлены без наследования, просто предоставив доступ к итератору. Все существующие алгоритмы работают.
  • Новые алгоритмы могут быть добавлены. Все контейнеры, поддерживающие эти итераторы, будут работать с этими новыми алгоритмами, прошлым, настоящим или будущим.

Ответ 4

Стандартная библиотека С++ (примечание: она не называется STL) имеет много существующих типов контейнеров: vector, array, deque, forward_list, list, set, map, multiset, multimap, unordered_set, unordered_map, unordered_multiset, unordered_multimap, stack, queue, priority_queue. Скорее всего, вы просто хотите использовать один из них напрямую - вы, конечно, никогда не хотите получать от них. Однако, конечно, возможно, что вам может понадобиться реализовать свой собственный тип контейнера в какой-то момент, и было бы неплохо, если бы он соответствовал некоторому интерфейсу, не так ли?

Но нет, нет абстрактных базовых классов, из которых вытекают контейнеры. Тем не менее, стандарт С++ обеспечивает требования к типам (иногда называемым концепциями). Например, если вы посмотрите раздел §23.2 стандарта С++ 11 (или здесь), вы найдете требования к контейнеру, Например, все контейнеры должны иметь конструктор по умолчанию, который создает пустой контейнер в постоянное время. Существуют более конкретные требования для Контейнеры последовательности (например, std::vector) и Ассоциативные контейнеры (например, std::map). Вы можете кодировать свои классы для удовлетворения этих требований, а затем люди могут безопасно использовать ваши контейнеры, как они ожидали.

Конечно, существуют требования ко многим вещам, кроме контейнеров. Например, стандарт предоставляет требования для разных типов итераторов, генераторов случайных чисел и т.д.


Ряд людей в комитете ISO С++ (действительно, 8-я Исследовательская комиссия) изучают возможность использования этих концепций языка. Это предложение позволит вам указать требования к типам, которые необходимо выполнить для их использования в качестве аргументов типа шаблона. Например, вы могли бы написать функцию шаблона примерно так:

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

Однако, я думаю, что это в настоящее время не понимает С++.

Ответ 5

В С++ коллекции (как контейнеры) и общие алгоритмы, которые работают на них, реализованы таким образом, что полностью не знают о наследовании. Вместо этого то, что их связывает, это итераторы: для каждого контейнера укажите, какую категорию итераторов он предоставляет, для каждого алгоритма укажите, с какой категорией итераторов он работает. Итак, в некотором смысле, итераторы "соединяют" два других вместе, и именно так STL позволяет сохранить количество контейнеров и алгоритмов до минимума (N + M вместо N * M). Контейнеры далее определяются как контейнеры последовательностей (вектор, дека, список (двойной связанный список) или forward_list (односвязный список) и ассоциативные контейнеры (карта, набор, хэш-карта, hashset и т.д.). Контейнеры последовательности связаны с производительностью (т.е. один из них - лучший выбор для другой ситуации). Ассоциативные контейнеры связаны с тем, как вещи хранятся в них и их последствиях (двоичное дерево против хэшированного массива). Аналогичные идеи применяются для алгоритмов. Это суть общего программирования, примером которого является STL потому что они специально и намеренно не ориентированы на объекты. Действительно, вам придется искажать чистый подход OO для достижения плавного генерического программирования. Такая парадигма не радует таких языков, как Java или Smalltalk