C++ tricks, #1: Compile time checks
While writing a library, you have sometimes to prevent users from making mistakes. To accomplish this, you have two choices: Either include run-time checking and throw an appropriate exception or try to catch as much as possible during compile time. Of course, the latter should be prefered when possible as it adds no runtime costs and forces users to use your library properly. Read on for a small example for compile time checks.
Situation
Let’s assume we have rolled out our own RTTI system and now we want to create a safe casting function. We only support single-inheritance to keep things simple. It should allow downcasting from a type to a further derived type, but not accross types. This means, if you have a class Main
and two derived types, Left
and Right
, it should be possible to cast from Main
to Left
or Right
but not from Left
to Right
(at least, not directly).
What can we check?
Well, let’s start what we cannot check. We have no way at compile time to see whether a Main
pointer points at runtime to a derived object or not, so we have to do this at runtime. What we can check though is if someone tries to assign a Left
object to a Right
pointer. How do we do that? We have to check if the target is derived from the source or the other way round - if so, one class is derived from the other. Moreover, we have to check if both are the same - this is always safe. Instead of implementing own is_base_of
checks, we’ll use the excellent boost/type_traits
library. Here’s the code of our checker:
#include <boost/type_traits/is_same.hpp>
#include <boost/type_traits/is_base_of.hpp>
struct Assign
{
template <bool CastIsValid> struct AssignImplementation;
template <> struct AssignImplementation <true>
{
template <class Left, class Right>
static void assign (Left* l, Right* r)
{
l = static_cast<Left*> (r); // Your runtime cast here
}
};
template <class Left, class Right>
static void assign (Left* l, Right* r)
{
AssignImplementation
<
boost::is_same<Left, Right>::value ||
boost::is_base_of<Left, Right>::value ||
boost::is_base_of<Right, Left>::value
>::assign (l, r);
}
};
template <class Target, class Source>
void safe_assign (Target* target, Source* source)
{
Assign::assign (target, source);
}
We check at compile time if the types can be casted, if so, we pass on to a partially specialized template (AssignImplementation
) which should do the run-time check. In this example, I just used static_cast
, for the sake of simplicity. If the compile-time check fails, AssignImplementation<false>
will be called, and as it does not exist it will result in a compile-time error like
error C2027: use of undefined type "Assign::AssignImplementation<CastIsValid>"
with
[
CastIsValid=false
]
Example
Some example code so you can try for yourself - should work with any recent standards-conforming C++ compiler.
class Main
{
};
class Left : public Main
{
};
class Right : public Main
{
};
class LeftDerived : public Left
{
};
int main (int argc, char* argv[])
{
Left* leftDerived = new LeftDerived ();
LeftDerived* leftDerivedPointer = 0;
Right* right = 0;
safe_assign (leftDerivedPointer, leftDerived);
safe_assign (right, leftDerived); // Error!
}