I'm wondering if it's a good idea to wrap C++ STL containers to maintain consistency and being able to swap the implementation without modifying client code.
For example, in a project we use CamelCase for naming classes and member functions (Foo::DoSomething()
), I would wrap std::list
into a class like this:
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;
};
Then I would be able to write something like this:
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;
}
// ...
I believe most of the modern C++ compliers can optimize away the extra indirection easily, and I think this method has some advantages like:
I can extend the functionality of the List class easily. For instance, I want a shorthand function that sorts the list and then call unique()
, I can extend it by adding a member function:
template<typename T>
void List<T>::SortUnique()
{
_list.sort();
_list.unique();
}
Also, I can swap the underlying implementation (if needed) without any change on the code they uses List<T>
as long as the behavior is the same. There are also other benefits because it maintains consistency of naming conventions in a project, so it doesn't have push_back()
for STL and PushBack()
for other classes all over the project like:
std::list<MyObject> objects;
// insert some MyObject's.
while ( !objects.empty() ) {
objects.front().DoSomething();
objects.pop_front();
// Notice the inconsistency of naming conventions above.
}
// ...
I'm wondering if this approach has any major (or minor) disadvantages, or if this is actually a practical method.
Okay, thanks for the answers so far. I think I may have put too much on naming consistency in the question. Actually naming conventions are not my concern here, since one can provide an exactly same interface as well:
template<typename T>
void List<T>::pop_back()
{
_list.pop_back();
}
Or one can even make the interface of another implementation look more like the STL one that most C++ programmers are already familiar with. But anyway, in my opinion that's more of a style thing and not that important at all.
What I was concerned is the consistency to be able to change the implementation details easily. A stack can be implemented in various ways: an array and a top index, a linked list or even a hybrid of both, and they all have the LIFO characteristic of a data structure. A self-balancing binary search tree can be implemented with an AVL tree or a red-black tree also, and they both have O(logn)
average time complexity for searching, inserting and deleting.
So if I have an AVL tree library and another red-black tree library with different interfaces, and I use an AVL tree to store some objects. Later, I figured (using profilers or whatever) that using a red-black tree would give a boost in performance, I would have to go to every part of the files that use AVL trees, and change the class, method names and probably argument orders to its red-black tree counterparts. There are probably even some scenarios that the new class do not have an equivalent functionality written yet. I think it may also introduce subtle bugs also because of the differences in implementation, or that I make a mistake.
So what I started to wonder that if it is worth the overhead to maintain such a wrapper class to hide the implementation details and provide an uniform interface for different implementations:
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();
}
}
};
Now, I just have to make sure that AVLTree::Find()
and RBTree::Find()
does exactly the same thing (i.e. take the value to be searched, return an iterator to the element or End()
, otherwise). And then, if I want to change from an AVL tree to a red-black tree, all I have to do is change the declaration:
AVLTree<MyObject> objectTree;
AVLTree<MyObject>::Iterator it;
to:
RBTree<MyObject> objectTree;
RBTree<MyObject>::Iterator it;
and everything else will be the same, by maintaining two classes.
I'm wondering if this approach has any major (or minor) disadvantages,
Two words: Maintenance nightmare.
And then, when you get a new move-enabled C++0x compiler, you will have to extend all your wrapper classes.
Don't get me wrong -- there's nothing wrong with wrapping an STL container if you need additional features, but just for "consistent member function names"? Too much overhead. Too much time invested for no ROI.
I should add: Inconsistent naming conventions is just something you live with when working with C++. There's just too much different styles in too much available (and useful) libraries.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With