9

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.

double-beep
  • 5,031
  • 17
  • 33
  • 41
PkmX
  • 119
  • 2
  • 10
  • 2
    Thinking about this idea is making me physically ill. – Chris Lutz Feb 08 '11 at 07:20
  • Dont see anything wrong in doing it. but do you really want to??? – Arunmu Feb 08 '11 at 07:26
  • So to get this straight, you want to do all of this just because of naming reasons and swapping the implementation? Naming could not be it because then you would wrap a lot of other STL things. – RvdK Feb 08 '11 at 07:28
  • 3
    you culd extend list as well by writing free functions like void SortUnique( std::list& l ){ l.sort(); l.unique() } – stijn Feb 08 '11 at 07:59

7 Answers7

7

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.

Martin Ba
  • 37,187
  • 33
  • 183
  • 337
3

Sounds like a job for a typedef, not a wrapper.

DrPizza
  • 17,882
  • 7
  • 41
  • 53
  • Completely agree. In fact, I always use typedef whenever I use a container. Not only to make it easier (easier, not automatically) to change containers later, but also to prevent repeating the full std::... definition over and over again (especially for iterators, although this is less of a problem now with the auto keyword). – Patrick Feb 08 '11 at 08:28
2

...being able to swap the implementation without modifying client code

At the problem-solving level of code, you may want to select a different internal container while presenting the same API. But, you can't reasonably have code that chooses specifically to use a List, then swap the implementation to anything else without compromising the performance and memory utilisation characteristics that led the client code to select a list to begin with. The compromises made in the Standard containers are well understood and well documented... it's not worth devaluing your programming staff's education, available reference material, increasing time for new staff to get up to speed etc. just to have a homegrown wrapper.

Tony Delroy
  • 102,968
  • 15
  • 177
  • 252
2

Any decent compiler will be able to make your wrapped classes as fast as the standard ones, however your classes may be will be less usable in debuggers and other tools that could have been specialized for standard containers. Also you are probably going to have error messages about compile time errors that are a bit more cryptic than the already cryptic ones that a programmer gets when making mistakes in the use of a template.

Your naming convention doesn't look to me any better than the standard one; it's actually IMO a bit worse and somewhat dangerous; e.g. it uses the same CamelCasing for both classes and methods and makes me wonder if you know what are the limitations imposed by the standard on a name like _list...

Moreover your naming convention is known only by you and probably a few others, instead the standard one is known by a huge number of C++ programmers (including you and hopefully those few others). And what about external libraries you may need to add to your project? Are you going to change their interface so that your wrapped custom containers are used instead of standard ones?

So I wonder, where are the pluses of using your custom naming convention? I only see downsides...

6502
  • 112,025
  • 15
  • 165
  • 265
  • As far as I am aware, the C++ standard only reserves names that begin with a underscore in the global namespace. [link](http://stackoverflow.com/q/228783/509880) Making the cryptic template error messages even more cryptic is a very good point though. – PkmX Feb 08 '11 at 11:36
  • This is exactly the limitation I was talking about. Like I said before that use is legal but IMO dangerous because the validity depends on the context. Putting underscore at the end is IMO a better idea. – 6502 Feb 08 '11 at 13:02
1

Wrapping STL containers to make them thread-safe, to hide away from the user implementation detail, to give the user limited functionality... these are valid reasons.

Wrapping one just because it does not follow your casing convention is a waste of time and money and could bring in bugs. Just accept that STL uses all-lowercase.

CashCow
  • 30,981
  • 5
  • 61
  • 92
0

No don't wrap them like that.
You wrap containers if you want to change what the containers are.
For example, if you have an unordered_map that should only add/remove elements internally but should expose the [] operator you wrap it up and create your own [] that exposes the internal container's [] and you also expose a const_iterator that is the unordered_map's const_iterator.

the_drow
  • 18,571
  • 25
  • 126
  • 193
0

In reply to your edit: I suggest you have a look at the Adapter Design Pattern:

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces. (Design Patterns, E. Gamma et Al.)

Then, you should change your point of view and use a different vocabulary: consider your class as a translator instead of a wrapper and and prefer the term "adapter".

You will have an abstract base class that defines a common interface expected by your application, and a new subclass for each specific implementation:

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 */ }
}

You can easily swap between the possible implementations:

ContainerInterface *c = new AVLAdapter ;
c->Find(...);
delete c;
c = new RBAdapter ;
c->Find(...);

And this pattern is scalable: to test a new implementation, provide a new subclass. The application code does not change.

class NewAdapter : public ContainerInterface
{
    virtual Iterator Find(...) { /* implement new method */ }
}
delete c;
c = new NewAdapter ;
c->Find(...);
Julien-L
  • 5,267
  • 3
  • 34
  • 51