Обертывание контейнеров для поддержания согласованности

Мне интересно, стоит ли оборачивать контейнеры 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 , если поведение остается таким же. Есть и другие преимущества, поскольку он поддерживает согласованность соглашений об именах в проекте, поэтому он не имеет 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's 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 () делает то же самое (т.е. берет значение для поиска, возвращает итератор элементу или End () в противном случае). А затем, если я хочу перейти от AVL-дерева к красно-черному дереву, все, что мне нужно сделать, это изменить объявление:

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

на:

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

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

10
задан double-beep 1 May 2019 в 15:40
поделиться