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.