Упаковка контейнеров для обеспечения согласованности

Мне интересно, если это хорошая идея, чтобы обернуть контейнеры C++ STL для обеспечения согласованности и возможности поменять местами реализацию без изменения клиентского кода.

Например, в проекте мы используем CamelCase для именования классов и функций-членов (Foo::DoSomething()), я бы обернул std::list в класс следующим образом:

template<typename T>
class List
{
    public:
        typedef std::list<T>::iterator Iterator;
        typedef std::list<T>::const_iterator ConstIterator;
        // more typedefs for other types.

        List() {}
        List(const List& rhs) : _list(rhs._list) {}
        List& operator=(const List& rhs)
        {
            _list = rhs._list;
        }

        T& Front()
        {
            return _list.front();
        }

        const T& Front() const
        {
            return _list.front();
        }

        void PushFront(const T& x)
        {
            _list.push_front(x);
        }

        void PopFront()
        {
            _list.pop_front();
        }

        // replace all other member function of std::list.

    private:
        std::list<T> _list;
};

Тогда я мог бы написать что-то вроде этого:

typedef uint32_t U32;
List<U32> l;
l.PushBack(5);
l.PushBack(4);
l.PopBack();
l.PushBack(7);

for (List<U32>::Iterator it = l.Begin(); it != l.End(); ++it) {
    std::cout << *it << std::endl;
}
// ...

Я полагаю, что большинство современных компиляторов C++ могут легко оптимизировать лишнюю косвенность, и я думаю, что этот метод имеет некоторые преимущества, такие как:

Я могу легко расширить функциональность класса List. Например, мне нужна сокращенная функция, которая сортирует список и затем вызывает unique(), я могу расширить его, добавив функцию-член:

 template<typename T>
 void List<T>::SortUnique()
 {
     _list.sort();
     _list.unique();
 }

Кроме того, я могу поменять базовую реализацию (при необходимости) без каких-либо изменений в коде, который они используют List<T> если поведение остается тем же. Есть и другие преимущества, потому что он поддерживает согласованность соглашений об именах в проекте, поэтому он не имеет push_back() для STL и PushBack() для других классов по всему проекту, например:

std::list<MyObject> objects;
// insert some MyObject's.
while ( !objects.empty() ) {
    objects.front().DoSomething();
    objects.pop_front();
    // Notice the inconsistency of naming conventions above.
}
// ...

Мне интересно, есть ли у этого подхода какие-либо серьезные (или незначительные) недостатки, или это действительно практический метод.

Хорошо, спасибо за ответы до сих пор. Я думаю, что я мог бы поставить слишком много на согласованность имен в этом вопросе. На самом деле соглашения об именах не являются моей проблемой, так как можно предоставить точно такой же интерфейс:

template<typename T>
void List<T>::pop_back()
{
    _list.pop_back();
}

Или можно даже сделать интерфейс другой реализации похожим на STL, с которым большинство программистов C++ уже знакомы. Но, во всяком случае, на мой взгляд, это скорее вещь стиля и не так уж и важна.

Что меня беспокоило, так это последовательность, позволяющая легко изменять детали реализации. Стек может быть реализован различными способами: массив и верхний индекс, связанный список или даже гибрид обоих, и все они имеют характеристику LIFO структуры данных. Самобалансирующееся двоичное дерево поиска может быть реализовано с помощью дерева AVL или красно-черного дерева, и они оба имеют среднюю сложность O(logn) для поиска, вставки и удаления.

Поэтому, если у меня есть библиотека дерева AVL и другая библиотека красно-черного дерева с различными интерфейсами, и я использую дерево AVL для хранения некоторых объектов. Позже я решил (используя профилировщики или что-то еще), что использование красно-черного дерева повысит производительность, мне нужно будет перейти к каждой части файлов, использующих деревья AVL, и изменить класс, имена методов и, возможно, аргумент заказывает своим красно-черным деревом аналоги. Вероятно, есть даже некоторые сценарии, что новый класс еще не имеет эквивалентной функциональности, написанной. Я думаю, что это может также привести к незначительным ошибкам также из-за различий в реализации или из-за того, что я ошибаюсь.

Итак, что я начал задаваться вопросом: стоит ли поддерживать такие классы-обертки, чтобы скрыть детали реализации и предоставить единый интерфейс для разных реализаций:

template<typename T>
class AVLTree
{
    // ...
    Iterator Find(const T& val)
    {
        // Suppose the find function takes the value to be searched and an iterator
        // where the search begins. It returns end() if val cannot be found.
        return _avltree.find(val, _avltree.begin());
    }
};

template<typename T>
class RBTree
{
    // ...
    Iterator Find(const T& val)
    {
        // Suppose the red-black tree implementation does not support a find function,
        // so you have to iterate through all elements.
        // It would be a poor tree anyway in my opinion, it just an example.
        auto it = _rbtree.begin(); // The iterator will iterate over the tree
                                   // in an ordered manner.
        while (it != _rbtree.end() && *it < val) {
            ++it;
        }
        if (*++it == val) {
            return it;
        } else {
            return _rbtree.end();
        }
    }
};

Теперь мне просто нужно убедиться, что AVLTree::Find() и RBTree::Find() делают одно и то же (т.е. принимают RBTree::Find() значение, возвращают итератор в элемент или End(), в противном случае). И затем, если я хочу перейти от дерева AVL к красно-черному дереву, все, что мне нужно сделать, это изменить объявление:

AVLTree<MyObject> objectTree;
AVLTree<MyObject>::Iterator it;

чтобы:

RBTree<MyObject> objectTree;
RBTree<MyObject>::Iterator it;

и все остальное будет таким же, поддерживая два класса.

Ответ 1

Мне интересно, есть ли у этого подхода какие-либо серьезные (или незначительные) недостатки,

Два слова: кошмар поддержания.

И тогда, когда вы получите новый компилятор С++ 0x с поддержкой перемещения, вам нужно будет расширить все классы-оболочки.

Не поймите меня неправильно - нет ничего плохого в упаковке контейнера STL, если вам нужны дополнительные функции, но только для "постоянных имен функций членов"? Слишком много накладных расходов. Слишком много времени инвестировано без рентабельности инвестиций.

Я должен добавить: Несогласованные соглашения об именах - это то, с чем вы живете, когда работаете с С++. Там слишком много разных стилей в слишком доступных (и полезных) библиотеках.

Ответ 2

Звучит как задание для typedef, а не для обертки.

Ответ 3

... возможность обмена версией без изменения кода клиента

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

Ответ 4

Любой достойный компилятор сможет сделать ваши завернутые классы такими же быстрыми, как стандартные, однако ваши классы могут быть менее полезными в отладчиках и других инструментах, которые могли быть специализированы для стандартных контейнеров. Также вы, вероятно, будете иметь сообщения об ошибках времени компиляции, которые являются немного более загадочными, чем уже загадочные, которые программист получает при ошибках при использовании шаблона.

Ваше соглашение об именах не выглядит лучше, чем стандартное; это на самом деле ИМО немного хуже и несколько опасно; например он использует тот же CamelCasing для обоих классов и методов и заставляет задуматься, знаете ли вы, какие ограничения накладываются стандартом на имя типа _list...

Кроме того, ваше соглашение об именах известно только вам и, возможно, нескольким другим, вместо этого стандартное известно большому числу программистов на С++ (включая вас и, надеюсь, тех немногих других). А как насчет внешних библиотек, которые вы можете добавить в свой проект? Вы собираетесь изменить свой интерфейс, чтобы вместо стандартных использовать ваши собственные пользовательские контейнеры?

Итак, интересно, где плюсы использования вашего пользовательского соглашения об именах? Я вижу только минусы...

Ответ 5

Обтекание контейнеров STL, чтобы сделать их потокобезопасными, чтобы скрыться от деталей реализации пользователя, чтобы предоставить пользователю ограниченную функциональность... это веские причины.

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

Ответ 6

Нет, не обматывайте их так. Вы переносите контейнеры, если хотите изменить, что представляют собой контейнеры.
Например, если у вас есть unordered_map, который должен только добавлять/удалять элементы внутри, но должен выставлять оператор [], который вы его завершаете, и создавать свой собственный [], который предоставляет внутренний контейнер [], а также выставлять const_iterator, который является unordered_map const_iterator.

Ответ 7

В ответ на ваше изменение: я предлагаю вам взглянуть на шаблон дизайна адаптера:

Преобразование интерфейса класса в ожидают другие клиенты интерфейса. Адаптер позволяет классам работать вместе что в противном случае несовместимые интерфейсы. (Дизайн Шаблоны, Э. Гамма и др.)

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

У вас будет абстрактный базовый класс, который определяет общий интерфейс, ожидаемый вашим приложением, и новый подкласс для каждой конкретной реализации:

class ContainerInterface
{
    virtual Iterator Find(...) = 0;
};
class AVLAdapter : public ContainerInterface
{
    virtual Iterator Find(...) { /* implement AVL */ }
}
class RBAdapter : public ContainerInterface
{
    virtual Iterator Find(...) { /* implement RB tree */ }
}

Вы можете легко переключаться между возможными реализациями:

ContainerInterface *c = new AVLAdapter ;
c->Find(...);
delete c;
c = new RBAdapter ;
c->Find(...);

И этот шаблон является масштабируемым: для проверки новой реализации создайте новый подкласс. Код приложения не изменяется.

class NewAdapter : public ContainerInterface
{
    virtual Iterator Find(...) { /* implement new method */ }
}
delete c;
c = new NewAdapter ;
c->Find(...);