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
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.
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.
-
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.Periata Breatta– Periata Breatta01/15/2017 20:43:30Commented 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– Christophe01/15/2017 21:25:10Commented 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.Frank Hileman– Frank Hileman01/16/2017 19:42:01Commented Jan 16, 2017 at 19:42
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.
-
Thank you for the reply! Can you give some examples or explain some design choices for case 3?Konstantin– Konstantin01/13/2017 22:31:57Commented Jan 13, 2017 at 22:31
-
Will there be different base template classes for different containers (set, map, vector?)Konstantin– Konstantin01/14/2017 09:18:42Commented Jan 14, 2017 at 9:18
-
1Could 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– Christophe01/15/2017 21:15:47Commented 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.Frank Hileman– Frank Hileman01/16/2017 19:44:48Commented 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.Frank Hileman– Frank Hileman01/16/2017 19:51:04Commented Jan 16, 2017 at 19:51
Explore related questions
See similar questions with these tags.