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(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::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 AutoReleasePtr(const AutoReleasePtr& autoreleaseptr, typename EnableIfTypesCanBeConverted::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& 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.//