C++ Interface Design – What a Double Container Should Offer

ccoding-standardsstl

I want to write a class which offers two sequences of elements to its users. The first one (lets call it "primary") is the main of the class and will be use 80% of the time. The second one (lets call it "secondary") is less important, but still need to be in the same class. The question is: what interface should the class offer to its users?

By looking at STL style, a class with a single sequence of elements should offer begin() and end() functions for traversal and function like insert() and erase() for modifications.

But how should my class offer the second sequence?

For now, I have two ideas:

  • Expose the two containers to the user (what about the Law of Demeter ?)
  • Provide the main container with STL interface and expose only the second one.

Here is an example.

#include <vector>

class A { 
    public:
        std::vector<int>&  primary();
        std::vector<char>& secondary();

    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;
};

class B { 
    public:
        std::vector<int>::iterator begin();
        std::vector<int>::iterator end();
        std::vector<char>& secondary();

    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;
};

// Classes implementation
// ...

int main() {

    // --------------------------------------------------
    // Case 1
    // --------------------------------------------------

    A a;

    for(auto it = a.primary().begin(); it != a.primary().end(); ++it) {
        // ...
    }   
    for(auto it = a.secondary().begin(); it != a.secondary().end(); ++it) {
        // ...
    }   

    // --------------------------------------------------
    // Case 2
    // --------------------------------------------------

    B b;

    for(auto it = b.begin(); it != b.end(); ++it) {
        // ...
    }   
    for(auto it = b.secondary().begin(); it != b.secondary().end(); ++it) {
        // ...
    }   
}

What is the more C++ish way to do that? Is one best than the other or is there an other solution?

Context

This problem came in the context of an exercise in which I am writing a simple database access framework. Here are the classes involved in the question:

  • table
  • column
  • row
  • field

The table class consists of a sequence of columns and an other sequence of rows. The main use of the table is manipulating (access, add and remove) the rows.

Deeper in the hierarchy, a row is made of column and field so a user can ask the value (field) corresponding to a given column or column name. Each time a column is add/modify/remove from the table, every rows will need to be modify to reflect the modification.

I want the interface to be simple, extensible and combining well with existing code (like STL or Boost).

Best Answer

Don't expose your guts, guide visitors :

class A { 
    public:

        // we assume you want read-only versions, if not you can add non-const versions
        template< class Func >
        void for_each_primary( Func f ) const { for_each_value( f, m_primary ); }

        template< class Func >
        void for_each_secodary( Func f ) const { for_each_value( f, m_secondary ); }

    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;

        template< class Func, class Container >
        void for_each_value( Func f, const Container& c )
        {
             for( auto i : c )
                 f( i );
        }
};



int main()
{
    A a;
    a.for_each_primary( [&]( int value ) 
         { std::cout << "Primary Value : " << value ; } );
    a.for_each_secondary( [&]( int value ) 
         { std::cout << "Secondary Value : " << value ; } )
}

Note that you could use std::function instead of template parameter if you want to put the implementation in a cpp file, making implementation changes less expensive on compilation times in big projects.

Also, I didn't try to compile it now, but I used a lot this pattern in my open-source projects.


This solution is a C++11 enhancement of B I guess.

HOWEVER

This solution have several issues :

  1. It requires C++11 to be effective, because it's efficient for the user of you class ONLY if he can use lambda.
  2. It relies on the fact that the class implementer really know what algorithms precisely are to be available to users. If the user need to do complex manipulations to the numbers, jumping from index to index in an unpredictable way for example, then exposing iterators, a copy of the values OR the values would be better.

In fact, this kind of choice totally depends on what you intend the user to do with this class.

By default I prefer the solution I gave you because it's the most "isolated" one, making sure the class know how it's values can be manipulated by external code. It's a bit like "extensions points". If it's a map, providing a find function to your class is easy. So I think that's the more sane way to expose data and it's also made available by lambdas.

As said, if you need to make sure the user can manipulate the data as he wish, then providing a copy of the container is the next "isolated" option (maybe with a way to reset the container with the copy after that). If a copy would be expensive, then iterators would be better. If not enough then a reference is acceptable but it's certainly a bad idea.


Now assuming you're using C++11 and don't want to provide algorithms, the most idiomatic way is using iterators this way (only the user code changes) :

class B { 
    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;
    public:

        // your code is read-write enabled... make sure it's not const_iterator you want
        // also I'm using decltypt to allow changing container type without having to manually change functions signatures
        decltype(m_primary)::iterator primary_begin() const;
        decltype(m_primary)::iterator primary_end() const;

        decltype(m_secondary)::iterator secondary_begin() const;
        decltype(m_secondary)::iterator secondary_end() const; 


};

int main()
{
    B b;


    std::for_each( b.primary_begin(), b.primary_end(), []( int& value ) {
        // ...
    });   
    std::for_each( b.secondary_begin(), b.secondary_end(), []( double& value ) {
        // ...
    });   

}
Related Topic