Упаковка контейнеров для обеспечения согласованности - PullRequest
10 голосов
/ 08 февраля 2011

Интересно, стоит ли оборачивать контейнеры 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'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;

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

Ответы [ 7 ]

6 голосов
/ 08 февраля 2011

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

Два слова: Технический кошмар.

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

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

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

3 голосов
/ 08 февраля 2011

Похоже, что работа для typedef, а не оболочки.

2 голосов
/ 08 февраля 2011

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

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

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

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

2 голосов
/ 08 февраля 2011

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

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

1 голос
/ 08 февраля 2011

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

0 голосов
/ 08 февраля 2011

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

Преобразование интерфейса класса в клиенты ожидают другого интерфейса. Адаптер позволяет классам работать вместе что не могло быть иначе из-за несовместимые интерфейсы. (Дизайн Patterns, E. Gamma et al.)

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

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

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(...);
0 голосов
/ 08 февраля 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...