Some time ago I wrote a simple mixin class template. A week later I found a little problem with it. Although I found a solution in a second I decided to analyse it more deeply. It's worth analysing further because it concerns some fundamental features of C++.
Here is the problematic code:
template<class T> struct Mixin : T { ~Mixin(); };
I guess I know your feelings. The class template looks like an example taken from a C++ book. You might have been taught with code like this. Your feelings about it most likely are based on unchallenged assumptions about simple C++ language constructs. Despite its basic nature the code has one little problem.
Why does this code look nice at first glance? Well, if it was an ordinary class you could just compile it and see that everything is fine. But the "just compile it" idea doesn't work in the case of class templates. Actually, writing the code is only half the job. The second half is instantiating the template. This will be done by the user unless you think of all possible cases and instantiate them in your tests.
This is a different way of thinking. If you deal with templates you should imagine how different instantiations could be compiled. You can tell me "Hey, what's the problem, I can write tests and instantiate the template there". Yes, you can. But first you have to find the right classes for instantiations. As an example, can you find an instantiation of Mixin<X> that breaks the code above?
Don't think too much, I have an answer. Here it is:
struct X { virtual ~X() throw(); };
Once the right class is found you can try to compile it. My compiler (g++ 3.2.2) complains:
1.cpp: In instantiation of 'Mixin<X>': 1.cpp:12: instantiated from here 1.cpp:3: looser throw specifier for 'void Mixin<T>::Mixin() [with T = X]' 1.cpp:7: overriding 'virtual X::~X() throw ()'
According to our best friend, the C++ standard [ISO], paragraph 15.4, bullet 3:
If a virtual function has an exception-specification, all declarations, including the definition, of any function that overrides that virtual function in any derived class shall only allow exceptions that are allowed by the exception-specification of the base class virtual function.
None is allowed in a destructor of base class X. Therefore, none should be allowed in a destructor of the derived class Mixin<X>:
template<class T> struct Mixin : T { ~Mixin() throw(); };
Well, we found a quick solution to the problem. Does it have some drawbacks? Can it break other instantiations? For example, what if T's destructor may occasionally throw? Mixin<X> has an empty exception specification list, therefore, std::unexpected will be called. This function will call std::terminate and program execution will be aborted. This is definitely not what a user wants.
Luckily, many C++ gurus recommend not throwing exceptions in the destructor at all. It's enough to mention in the documentation of Mixin that the destructor of T must meet the Nothrow requirement.
It seems that the problem is solved. Indeed, if you're a bug hunter who has just ended up with code like that above you can stop reading here. I'd rather analyze it a little bit more.
What is annoying me in a destructor with an empty exception specification is the fact that a compiler may put the destructor's code into a try-catch block. It protects your application against "exception leaks". The try-catch block can be omitted only if the destructor's body is available and the compiler can deduce that the destructor never throws. Otherwise, unnecessary try-catch blocks make the code bigger and execution slower.
Another inconvenience of the code was suggested by Phil Bass while reviewing this article. His concern is a design flaw rather than implementation details. Phil suggested that, if Mixin is part of a general-purpose library, it would be great if Mixin were to follow a project-specific exception specification policy.
There are two major exception specification policies used in destructors:
-
No exception specification at all
-
Empty exception specification
Probably, the first policy is used more widely than the second. I would say both are used in C++ projects. For example, the C++ standard library uses both.
Needless to say, a Mixin<T> destructor that is neutral to the exception specification policy of T is preferred rather than a destructor that forces using either choice.
I recommend that you stop reading for a moment and try to find a best-of-all-worlds solution. A solution that is free from the limitation of the first version of Mixin and that doesn't dictate a particular exception specification policy.
Although you have little freedom in defining the destructor the solution may surprise you. It is no destructor at all, that is, an implicitly defined destructor:
template<class T> struct Mixin : T { };
Why is this better? To explain why, let me refer you to [1], paragraph 15.4, bullet 13. Apart from an explanation of our case it contains an example with multiple inheritance, which we'll analyze later. In my informal interpretation, an implicitly defined destructor "inherits" its exception specification from the base destructor. Whatever exception specification T's destructor has so has an implicitly defined destructor of Mixin<T>. Perfect, exactly what we need!
You may ask how to keep it implicitly defined in real class templates. I recommend that you use RAII wrappers, smart pointers, C++ strings and containers wherever you can. This reduces the need for explicitly defined destructors to very unusual cases.
Now it's time to solve the problem I faced. It's almost the same as our original problem with one difference - Mixin has an additional base:
struct Base { // ... }; template<class T> struct Mixin : Base, T { // ... };
It's clear that we can always use a nothrow destructor in Mixin. I'd like you to analyze the case of an implicitly defined destructor. Just remember that, on the one hand, an implicitly defined destructor inherits exception specifications from all its bases, and on the other hand, if any of the base destructors is virtual, ~Mixin() can't have a less restrictive exception specification. The analysis is a kind of combinatorial puzzle. You can combine the virtuality and exception specifications of all the destructors. Fortunately, there are only a few combinations.
The first case is a non-virtual destructor ~Base(). The analysis shows that the destructor of Base has to have an empty exception specification in order to define ~Mixin() implicitly.
struct Base { ~Base() throw(); }; template<class T> struct Mixin : Base, T { };
Although this solution dictates the exception specification policy of the Base destructor, it's still of interest because the resulting Mixin class template is neutral to the user's exception specification policies.
The second case doesn't have a solution. If Base's destructor is virtual we can always find a type T that breaks the compilation regardless of the exception specification of ~Base().
This was my case. I could take the destructor's virtuality out of the base into another class responsible for polymorphic cloning and destruction (let's say, storage management). Although it would better fit the one class, one responsibility principle I decided to use a quick fix solution:
struct Base { virtual ~Base(); }; template<class T> struct Mixin : Base, T { ~Mixin() throw(); };
I'd like to draw two conclusions. First, a summary of what has been done.
Mixin classes often come with general-purpose libraries or libraries that make no assumptions about the projects that will use them. It's important to follow the project's rules and policies even when a set of projects is unknown to the library author. In this article I showed how to solve one particular problem with respect to possible uses of your code.
The second conclusion is rather philosophical. Although you can rarely find code simpler than that discussed in this article it's worth analyzing it. I dare say there is no such thing as a little detail in C++. Everything is important in the C++ world. If you find an interesting note on a C++ feature or some side effect, try to play with it. Many C++ tricks and modern techniques were discovered this way. Keep trying! Together we'll make a better language.
Overload Journal #60 - Apr 2004 + Programming Topics
Browse in : |
All
> Journals
> Overload
> 60
(8)
All > Topics > Programming (877) Any of these categories - All of these categories |