Jump to content

April 2017 »

232425262728 29
- - - - -

Some basic C++ template metaprogramming

4: Adsense

As much as I hate the language, I have to admit that C++ has an impressive amount of power under the hood - even though accessing that power sometimes requires some insane contortions. In particular, I'm always interested in the tricks that people come up with for template metaprogramming. They are usually perverse, twisted, and bizarre. Sometimes they are outright evil. It's gross what one has to do to really apply C++ to its fullest, but the fact that such a primitive language can do such things at all is still pretty impressive.

I ran into a situation the other day involving templated copy constructors, where the template was allowing some weird implicit conversions to take place that it really shouldn't have permitted. (For instance, converting from a FooClass* to a double. WTF, C++. WTF.)

Of course in a nicer language I would be able to trap and prevent such conversions easily; but C++ is, naturally, not a nice language. So I had to dig into the bag of magical goodies and whip out some high-powered voodoo.

Along the way, I realized that the workings of the code were pretty arcane, especially to newer C++ programmers. In the interest of helping my colleagues understand all the gibberish in the new code, I went through and heavily documented exactly how all of it works.

Hopefully, you'll find this interesting and informative [smile]

* \file
* Helper templates for conditionally enabling code
* Adapted (lightly) from boost::intrusive_ptr and boost::detail::sp_enable_if_convertible
* \date 2010-01-30 (18:58)
* \author Mike Lewis

#pragma once

// This struct is the core of the conversion test. It detects if the type
// given by T1 can be converted into the type given by T2.
// The principle is fairly simple: we define two overloads of the function
// TestDummy. One overload accepts a pointer to a T2 object, and the other
// is a simple variadic function, i.e. the compiler will allow us to pass
// virtually anything to it. Each overload also returns a different helper
// type, "yes" or "no." These helpers have different sizes so that we can
// distinguish between them at compile time using the sizeof operator.
// The actual check is managed by the PerformConversionCheck enum. This
// enum contains a single value, CheckResult. The value will be true if
// the type conversion is allowed, or false otherwise. In order to pick
// the correct value, we ask the compiler for the size of the return value
// of the TestDummy function. We do this by passing a pointer of type T1
// into the TestDummy function. If the conversion from T1 to T2 is legal,
// the compiler will choose the first TestDummy overload, which returns
// the "yes" helper type. If the conversion is not possible, the compiler
// will instead use the variadic overload of TestDummy, which returns the
// special "no" helper type. Once this overload resolution is complete,
// the compiler knows the correct size of TestDummy's return value. If
// that size is equal to the size of the "yes" helper type, we know that
// the compiler selected the first TestDummy overload, and therefore we
// know that converting from T1 to T2 is legal.
template<class T1, class T2>
struct CanConvertTypes
typedef char (&yes)[1];
typedef char (&no)[2];

static yes TestDummy(T2*);
static no TestDummy(...);

enum PerformConversionCheck { CheckResult = (sizeof(TestDummy(static_cast<T1*>(NULL))) == sizeof(yes)) };

// This dummy structure is used later to flag "enable-if" results that are accepted.
// If the helper template (see below) provides the correct typedef, then we can
// assign an unnamed temporary EnableIfDummy to the helper, which means the code will
// compile cleanly. If the typedef is not available, the assignment will fail, and
// trigger the compiler logic that makes the static check possible (see final notes).
struct EnableIfDummy { };

// This helper template is specialized based on a boolean value. When the provided
// value is true, the template specialization contains a typedef for EnableIfDummy.
// If the provided value is false, that typedef is not present, so any code that
// tries to use the typedef will fail to compile.
template<bool> struct EnableIfTypesCanBeConvertedHelper;
template<> struct EnableIfTypesCanBeConvertedHelper<true> { typedef EnableIfDummy type; };
template<> struct EnableIfTypesCanBeConvertedHelper<false> { };

// This struct is the public interface for performing an "enable-if" check. The two
// provided types are passed along to the CanConvertTypes struct; the function of
// that struct is detailed above. The final enum value CheckResult will be true or
// false, depending on whether or not the type conversion is legal. This true/false
// flag directs the compiler to select the corresponding specialization of the helper
// template, EnableIfTypesCanBeConvertedHelper. By connecting these pieces of logic,
// we will end up deriving the conversion class from either the true or false version
// of the helper template. If the true version is selected, it will contain a typedef
// that can be used by the calling code; otherwise, that typedef is invalid. The
// code using this template explicitly requests to use that typedef, meaning that if
// the typedef is present, the code will compile; otherwise, it will not. The final
// result is that we can disable code from working if the types can't be converted
// as the caller requests.
template<class T1, class T2>
struct EnableIfTypesCanBeConverted : public EnableIfTypesCanBeConvertedHelper<CanConvertTypes<T1, T2>::CheckResult> { };

// FINAL NOTES: an example, and how the compiler can give us useful error messages
// even though we're doing a lot of template magic to make this all work.
// Consider the following example, where we want to allow a conversion constructor
// for some types, but disallow it for others, specifically we only want to allow
// conversion of the wrapper class if the nested pointers can also be converted
// legally:
// template<class T2> AutoReleasePtr(const AutoReleasePtr<T2>& autoreleaseptr, typename EnableIfTypesCanBeConverted<T2, T>::type safetycheck = EnableIfDummy());
// As detailed above, if the type conversion is legal, the typedef "type" will be
// available, and therefore the above code will compile. If the conversion is not
// legal, the code will not compile.
// The trick is a special rule in C++ called "Substitution Failure Is Not An Error",
// commonly referred to as SFINAE. This rule states that if a template overload
// fails to compile, the compiler should silently ignore this failure and continue
// to look for other overloads that work correctly. (This applies to things beyond
// just function overloading, but that's out of the scope of what we need to do
// for this particular code.)
// Since the enable-if check produces invalid code when the check fails, the compiler
// will trigger SFINAE. It will attempt any other conversions it can; if any of those
// are allowed, then the code compiles, and even better, it compiles with the correct
// conversion code for us. However, if no conversions can be generated, the compiler
// must fall back on the original copy constructor:
// AutoReleasePtr(const AutoReleasePtr<T>& autoreleaseptr);
// But that copy constructor can't be compiled with an illegal coversion involved! So
// the compiler gets this far without complaining to us, but now it is totally stuck.
// Since the conversion is illegal, the compiler will say that the conversion from one
// AutoReleasePtr type to another is not allowed. The template parameters for each
// pointer wrapper are also displayed, so we can immediately see that the cause of a
// compile error in this case is because of an invalid conversion between the raw
// pointer types.
// And voila! We have successfully enabled (or disabled) a piece of code, based on
// whether or not a type conversion is valid. Best of all, all this is compiled away
// and involves no run-time overhead, so there is no cost to using this trick.

Feb 01 2010 08:33 AM
Hmmm... seems like a complex alternative to static_cast? Although maybe the real bug was the template parameters being supplied?
Feb 01 2010 11:29 AM
static_cast doesn't provide the right semantics for what we need to do in this situation.

Specifically, consider the following scenario:

struct Base

int foo;

struct Derived : public Base
float bar;

void frobnicate()
Base b;
Derived* pderived = static_cast<Derived*>(&b); // Uh oh!

static_cast permits casts both up and down an inheritance tree, regardless of whether or not the cast is valid. (The idea is that you're convincing the compiler to stop doing type checking for a second; that's essentially what the C++ casts mean.) To get proper and safe casting, you'd have to use dynamic_cast, which of course doesn't prevent compile-time mistakes like the one above.

By contrast, the semantics of the enable-if method only permit legal casts up the inheritance hierarchy. In our case, the code involved is a smart pointer. If we used nothing but a static_cast, the following would compile without any errors or warnings:

AutoReleasePtr<Base> baseptr = GetSomeBaseObject();

AutoReleasePtr<Derived> derivedptr = baseptr;

This is obviously wrong by eye, but the compiler will allow it, because of the semantics of static_cast.

The use of enable-if logic is more than justified here, as it would be an expensive, hellish operation to find all the bugs that occur with stuff like the base/derived example above. By using a little bit of magic, we can get compile-time type safety guarantees, and zero runtime overhead.
Feb 01 2010 01:41 PM
Doh I didn't realise static_cast allowed you to go down the heirachy but it makes sense. But your code is definitely a prime example of template magic :)

Note: GameDev.net moderates comments.