During the transition to the modern C++ of the ISO standard, several additions were made to the language. Two of these - exceptions and templates - have above all others influenced the way C++ interfaces are designed.
Three observations about templates in modern C++:
-
They are the principal C++ mechanism for expressing a static (that is, compile time) separation of four concerns - types, data structures, operations and control flow are separable and independently interchangeable.
-
A further level of indirection is introduced between the design coded by the programmer, and the code produced once the template is instantiated.
-
The compiler now has the capability to write C++ code for the programmer! That is, the compiler can accept input in the form a powerful meta-language called a template, which tells the it how you want your generated C++ code to look.
The rest of this article presents an example of each of these points in turn.
Compile Time Separation of Concerns
The way this normally comes across is in the commonality/variability separation, because a template is normally viewed in terms of one aspect - in other words, one concern - being varied while everything else remains constant. For example, the C++ Standard Library sequenced container class templates are representative of the separation of two of the four concerns mentioned above: types and structures. Consider the following implicit specialisation of the list container class template provided by the C++ standard library:
std::list<int> list_of_ints;
Here, the contained type, int , is the variable part, while the machinery for managing a list of int values - that is, the structure - is common and could manage a list of any type.
Although in the above example only two of the four concerns were present, use of the C++ Standard Library algorithms is often representative of all four concerns being present at the same time.
Consider the process of searching for a particular object within a container. The standard library provides an algorithm called find_if() , which provides an abstraction of control flow. The container and contained type are included indirectly via a pair of iterators. In addition, find_if() takes a function object as a parameter: this function object is an example of an operation (to be performed within the flow of control).
This logic is applicable regardless of the type of element of the sequence, and the implementation of the sequence could be any type of sequenced container, or could even be a file from which the elements are read. The operation of determining whether or not the sought element has been found or not is delegated to a function object.
Compile Time Indirection
Suppose, within a namespace called whatever, we wish to write a function called some_function() which makes use of a stack of integers. To simplify the implementation of some_function() , we wish to factor out some of the processing, delegating it to a function called uses_stack() . Given that the purpose of uses_stack() is just to absorb factored out processing, it makes sense to declare it locally in the anonymous namespace within the implementation file.
This leaves some_function() looking like this:
namespace whatever { void some_function() { typedef std::stack<int> stack_type; stack_type the_stack; uses_stack(the_stack); // ... and the rest ... } }
Here we face a problem: how to declare the function uses_stack() , because the parameter type is not know until within the block scope where it is actually being called.
One solution is just to hoist
typedef std::stack<int> stack_type;
to the top of the implementation file, and place it also in the anonymous namespace. This has the advantage of being simple, but at the same time, it removes from some_function() something which is an implementation detail of that function.
There is another solution: by using templates, and introducing another level of indirection at compile time, the declaration of uses_stack() can be deferred until the point at which it is called!
The code now looks like this:
namespace { template <typename type> void uses_stack(std::stack<type>& s); } namespace whatever { void some_function() { std::stack<int> the_stack; uses_stack(the_stack); // ... and the rest ... } }
The definition of uses_stack() is omitted for brevity (where it is placed is a matter of preference, as in this case it is declared, defined and used all within a single implementation file).
The type of the_stack - and note that the introduction of a template has rendered the use of the typedef unnecessary - can now be placed where we'd like it. Naturally though, nothing comes for free, and we now have a different set of tradeoffs to consider. The plus points have already been mentioned in the discussion leading up to here; however there is a minus point in that the function uses_stack() is generalised - and made generic - even though in usage terms, no generalisation is required. This is over generalisation, because no generalisation was necessary, regardless of the usefulness of the function template as a means of indirection. Therefore the code has become more complex in one respect, whereas it has become simpler in another (the typedef is now encapsulated within uses_stack() ).
This is quite a simple example of the use of a template to introduce a level of compile time indirection: a far more striking example can be found in an article by Alan Griffiths [ ARG ].
Code Generation
The place where the power of templates becomes most apparent is in the use of the C++ compiler as a C++ code generator ! Indeed, in their recent book [ C_E ] authors Czarnecki and Eisenecker devote lots of space to this code generation approach.
By way of example consider the software to control a video recorder. Different models have different features, and here let's consider the inclusion or not of the ability to respond to the PDC signal.
The VCR which can handle the PDC (Programme Delivery Centre) signal will be more expensive: what are you actually paying for when you pay more money? Two things spring to mind (not exhaustive of course): more onboard memory, and extra software. Therefore our design goals include excluding the PDC handling functionality from the onboard s/w of the cheaper model. With all this in mind, the software will be designed in terms of a common structure, and within this, variability will be implemented by policy classes [ AA ], which will be parameters in the interfaces of the classes which they help implement.
The PDC handling code is therefore factored into the following policy class templates (actually structs - the use of structs is typical for policies):
template <bool has_pdc> struct pdc_feature { static void handle_pdc() { : /* code */ : } }; // and specialisation: template <> struct pdc_feature<false> { static void handle_pdc() { /* Empty! */ } };
Another feature common to more expensive models is extra fast forward and rewind. Imagine we factor the code to handle this in a similar way, and we also have the following enum:
enum vcr_model_type { basic_model, middle_of_the_road_model, deluxe_model };
And the configuration templates:
// No general case, // each needs a specialisation template <vcr_model_type vcr_model> struct vcr_feature_configuration; template <> struct vcr_feature_configuration< middle_of_the_road_model > { static bool const has_pdc = false; static bool const has_fast_winding = true; }; template <> struct vcr_feature_configuration< deluxe_model > { static bool const has_pdc = true; static bool const has_fast_winding = true; };
Then we can base the onboard VCR s/w around operations expressed in the template:
template <vcr_model_type vcr_model> struct vcr_onboard_software { typedef typename vcr_feature_configuration<vcr_model> configuration; static bool const has_pdc = configuration::has_pdc; static void handle_pdc() { pdc_feature<has_pdc>::handle_pdc(); } // ... And the same sort of // thing for fast winding ... };
So we can now generate the software in its configuration for the deluxe model by instantiating the template
vcr_onboard_software<deluxe_model> delux_model_vcr_sw;
and similarly for the other models.
Summary
Consider again, the second example illustrating compile time indirection. In this example, as a consequence of making uses_stack() into a function template, the separation of structure and type concerns spilled over into users_stack() 's interface. Further, the extra level of indirection was available because a declaration of uses_stack() with a stack of integers as its parameter type:
void uses_stack(std::stack<int>& s);
was generated after the point where the parameter type became known!
This article's introduction presented three observations about the presence of templates in C++. Each of these observations presents one aspect of templates, none of which are independent of the others - they are all present to some degree in any C++ code that uses templates.
References
[AA] Andrei Alexandrescu: Modern C++ Design: Generic Programming and Design Patterns Applied , Addison-Wesley 2001.
[ARG] Alan Griffiths: Compile Time Indirection - An Unusual Template Technique, Overload 42 .
[C_E] Krzysztof Czarnecki and Ulrich W. Eisenecker: Generative Programming: Methods, Tools and Applications , Addison-Wesley 2000.
[GoF] Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides: Design Patterns: Elements of Reusable Object-Oriented Software , Addison-Wesley, 1995.