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