Мне интересно, стоит ли оборачивать контейнеры 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;
, и все остальное будет таким же, поддерживая два класса.