1

Suppose that there are multiple classes (let's call them Container-s) that have somewhat similar structure. They are smart containers for some other Foo-s classes.

These Container-s have:

[1] A single private STL container (vector, set, map) for objects of some other class Foo

[2] Standard public operations to work with the container in terms of Foo class (Add(FooObject), Remove(FooObject), IsMember(FooObject), iter Begin(), Clear() etc.)

[3] Public operations with standard names and arguments specific to the Foo class. For example, Add(int id, int profile) may actually create a Foo object inside Container and add it to its private STL container

[4] Public operations with non-standard names, for example GetNumberOfRedFooObjects() that exist only for the specific class Foo

class SomeContainer {

private:

    std::vector<SomeFoo> __someFoos; [1]

public: 

    (about 5-30 functions)

    void Add(const SomeFoo &foo);    [2]

    void Add(int id, int profile);   [3]

    int GetNumberOfRedSomeFoos();    [4]

}

Question 1: What good design choices/guidelines are available for such Container-s given that there are many of them (tens, hundreds)? Should I write each Container class from scratch? Should I implement some templated base class(es) for such Container-s?

Question 2: Is it fine to have methods of both [2] and [3] types or I should stick with just [2]?

The language is C++, but I think it's a more or less language agnostic question. Thank you for your help in advance!

2 Answers 2

1

This is a good candidate for applying the policy based design, as advocated in Alexandrescu's Modern C++ Design. It's based on the strategy design pattern, but at compile time, using templates.

The principle is to define your SmartContainer as a template. Its parameters shall be "policies" that specify some aspects of the behavior.

Step 1: define your policy driven class

Here a simplified example, with SmartContainer using a first template parameter to indicate the standard container type to be used. Fortunately, these parameters can be templates themselves. A second parameter defines the type of the elements:

template <template <typename...> class C, class T>
class SmartContainer {  
    C<T> mycontainer;   
    Adder<C,T> a; 
public:  
    void add (const T& element) { ... } 
    int get_number_of_elements() { return mycontainer.size(); } 
    void print() { for (auto &x:mycontainer) 
                       cout << x<<endl;  
                   cout<<endl; }
};

In your calling code you could then instantiate this using any standard container you want:

SmartContainer<vector, Item> c1;  // will use a vector of items
SmartContainer<list, Item> c2;    // will use a list of items

Step 2: use helper classes

Getting the size in this SmartContainer is similar for all kind of containers envisaged here. But the implementation add() function might seriously depend on the container chosen.

When you face this kind of issue, define a helper class:

template <template <typename...> class C, class T>
class Adder {
public:
    void operator() (C<T>& a, const T& element);  
};

The nice thing with template classes is that you may provide partial specializations:

template <class T>  // only one parameter is still flexible 
class Adder<vector, T> {  // the first parameter is fixed with vector
public: 
   void operator() (vector<T>& a, const T& element) {
       a.push_back(element);  //ok, as expected
   } 
};

template <class T>
class Adder<list, T> {
public: 
   void operator() (list<T>& a, const T& element) {
       a.push_front(element);   // it can be totally different.
   } 
};

You can now add the implementation of the generic add() function:

void add (const T& element) { a(mycontainer, element); } 

Thanks to the partial specializations, you now have a very flexible SmartContainer. If you want to use a new kind of standard container, you just have to provide a partial specialization for it, following the logic demonstrated above.

Online demo

Step 3: parametrize behavior that should be flexible

For every behavior that needs some flexibility, use an additional template parameter. In the example above, instead of referring to the Adder class directly in the template, you could make it a policy:

template <template <typename...> class C, class T, template template <typename...> class C, class T> class A>
class SmartContainer {  
    C<T> mycontainer; 
    A<C,T> a;             // <=== additional policy 

You could then use different "adders", like for example a front adder, that would be usable only for containers which permit it.

Step 4: become a template expert

Next step is to learn to use type traits, so that you could take into account properties of the template parameters to fine-tune your policies and the SmartContainer class.

Alexandrescu's book would be a good start. One of his case is a SmartPointer class, with a storage strategy (e.g. specialized strategies for short objects, etc...), an ownership handling strategy, a pointer conversion strategy, and a consistency checking strategy.

3
  • While I can see the motivation behind this approach, I have to say that in most circumstances Frank Hileman's solution is much easier to understand and less work to implement. Maybe for implementing a framework this would be the best approach, but I think usually the simpler way would be better. Commented Jan 15, 2017 at 20:43
  • @PeriataBreatta my solutions allows to implement a smart container for any type, using any container type (e.g. stack, vectors, sets, which have different interfaces for adding elements). The container specific code is managed via partial template specialization. What Frank proposes it to use inheritance from a template. In this case if the derived if not a template, you'd have to fix the type that can be stored. If the derived is itself a template, you'd need to implement the container specific behavior in the member override. This will be as much code than the partial specialization.
    – Christophe
    Commented Jan 15, 2017 at 21:25
  • Christophe's solution is more useful when the container type will vary, and the operations on the container are specialized, or when there is reusable code in the helper class. For an object model whereby the same type of container is used for everything, there may be no gain. His approach is more flexible and more orthogonal. It just depends on what you need. Commented Jan 16, 2017 at 19:42
4

A template base class is the best solution. This way you can avoid redundant code specific to case 2 (standard container methods) while still supporting case 3 in derived classes.

As to whether you should add "public operations with standard names and arguments specific to the Foo class" (case 3), it really depends on your usage of these classes. These can be considered convenience methods, so the code could just as easily go elsewhere.

5
  • Thank you for the reply! Can you give some examples or explain some design choices for case 3?
    – Konstantin
    Commented Jan 13, 2017 at 22:31
  • Will there be different base template classes for different containers (set, map, vector?)
    – Konstantin
    Commented Jan 14, 2017 at 9:18
  • 1
    Could you please show how the template base class could implement case 2, i.e. standard container operations (knowing that these may be different for vectors, stacks, and sets), and how you could then derive the classes defining the real container in the derived class ?
    – Christophe
    Commented Jan 15, 2017 at 21:15
  • @Konstantin case 3, an Add operation would be in the base class, so it does not specify the type of object added to the container. You would need different base template classes for different containers. Commented Jan 16, 2017 at 19:44
  • @Christophe If you do not want a private, container type specific field in the base class, you can make a base class with all the generic, type parameterized methods needed (Add, Remove, etc), all virtual, and use a derived class to specify the exact behavior. However what are you gaining? You are only gaining the specification of the operations in the base class. In this case I suggest the approach you outlined, as it breaks these things out separately: generic operations, container type, and operation implementations. Commented Jan 16, 2017 at 19:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.