These are not the containers you're looking for

| 0 Comments

STL containers are great. Rather than have to worry about writing doubly linked lists, efficient maps, and other such data structures you just grab one from the STL and you're away.

Unfortunately the STL containers have quite a large 'surface area'. Their interfaces are rich because they are generic containers. Often the container you actually need is much more limited in scope and in such situations I always find it's worth wrapping the STL container and providing a more appropriate interface to the user.

Suppose we are required to maintain a collection of widgets. We need to store the widgets by name, the widgets are dynamically allocated, we only need to store widgets by name, know if they're stored, and look them up. We decide to use a std::map<std::string, CWidget *> and even typedef it so that it's easy to use. The problem with this situation is that the code that uses the widget collection is working with the generic interface of the map and not the far more restricted logical interface of the widget collection. A std::map has lots of methods; our widget collection only really has 3. Performing any one of the three things we need to use our widget collection for is likely to result in multiple lines of code to manipulate the map. This leads to errors and code that requires more thinking than is necessary to understand; all because we're missing an abstraction.

The problem is that the widget collection is not a map it's a collection that is implemented in terms of a map. In such situations I tend to write a small, thin, collection class that does just what I require. Something like this, perhaps;

class CWidgets
{
   public :

      CWidgets() {}
      
      ~CWidgets()
      {
         for (Widgets::iterator it = m_widgets.begin(); it != m_widgets.end(); ++it)
            delete it->second;
      }

      void AddOrUpdate(
         const std::string &name,
         CWidget *pWidget)
      {
         Widgets::iterator it = m_widgets.find(name);

         if (it == m_widgets.end())
         {
            m_widgets.Insert(Widgets::value_type(name, pWidget));
         }
         else
         {
            delete it->second;
            it->second = pWidget;
         }
      }

      bool Contains(
         const std::string &name)
      {
         return m_widgets.find(name) != m_widgets.end();
      }

      CWidget *Find(
         const std::string &name)
      {
         CWidget *pWidget = 0;

         Widgets::iterator it = m_widgets.find(name);

         if (it != m_widgets.end())
         {
            pWidget = it->second;
         }

         return pWidget;
      }

   private :

      typedef std::map<std::string, CWidget *> Widgets;

      Widgets m_widgets;

      // no copies - do not implement
      CWidgets(const CWidgets &rhs);
      CWidgets &operator=(const CWidgets &rhs);     
};

This class presents a much smaller interface than the bare map. The functions all do exactly what we need. Multiple lines of code at the call site has been reduced to a single function call and the function has a name that clearly expresses the intent of the programmer. We can't clear the map by mistake and leak memory, we cant access the map elements incorrectly and we can write some simple tests to prove that everything works how we expect. We've taken the wide, generic interface that the map presents us with and narrowed it to the specific interface that we need; all this by adding a simple abstraction.

Leave a comment