Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wrapping Containers to Maintain Consistency

Tags:

c++

containers

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.

like image 590
PkmX Avatar asked Feb 08 '11 07:02

PkmX


1 Answers

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.

like image 185
Martin Ba Avatar answered Oct 20 '22 06:10

Martin Ba