Compile-time selection
To simulate conditionals at compile time, you can use the conditional ternary operator in an enum declaration. The following program uses this technique to calculate the maximum of two integers at compile time.
//: C05:Max.cpp
#include <iostream>
using namespace std;
template<int n1, int n2>
struct Max {
enum {val = n1 > n2 ? n1 : n2};
};
int main() {
cout << Max<10, 20>::val << endl; // 20
} ///:~
If you want to use compile-time conditions to govern custom code generation, you can once again use specializations of the values true and false:
//: C05:Conditionals.cpp
// Uses compile-time conditions to choose code
#include <iostream>
using namespace std;
template<bool cond>
struct Select {};
template<>
struct Select<true> {
static inline void f() { statement1(); }
private:
static inline void statement1() {
cout << "This is";
cout << " statement1 executing\n";
}
};
template<>
struct Select<false> {
static inline void f() { statement2(); }
private:
static inline void statement2() {
cout << "This is";
cout << " statement2 executing\n";
}
};
template<bool cond>
void execute() {
Select<cond>::f();
}
int main() {
execute<sizeof(int) == 4>();
} ///:~.
This program is the equivalent of the expression:
if (cond)
statement1();
else
statement2();
except that the condition cond is evaluated at compile time, and the appropriate versions of execute<>( ) and Select<> are instantiated by the compiler. The function Select<>::f( ) executes at runtime, of course. A switch statement can be emulated in similar fashion, but specializing on each case value instead of the values true and false.
Compile-time assertions
In Chapter 2 we touted the virtues of using assertions as part of an overall defensive programming strategy. An assertion is basically an evaluation of a Boolean expression followed by a suitable action: do nothing if the condition is true, or halt with a diagnostic message otherwise. The previous section showed how to evaluate compile-time Boolean expressions. The remaining challenge in emulating assertions at compile time is to print a meaningful error message and halt. All that is required to halt the compiler is a compile error; the trick is to insert helpful text in the error message. The following example of Alexandrescu[70] uses template specialization, a local class, and a little macro magic to do the job.
//: C05:StaticAssert.cpp
//{-g++}
#include <iostream>
using namespace std;
// A template and a specialization
template<bool>
struct StaticCheck {
StaticCheck(...);
};
template<>
struct StaticCheck<false>{};
// The macro (generates a local class)
#define STATIC_CHECK(expr, msg) { \
class Error_##msg{}; \
sizeof((StaticCheck<expr>(Error_##msg()))); \
}
// Detects narrowing conversions
template<class To, class From>
To safe_cast(From from) {
STATIC_CHECK(sizeof(From) <= sizeof(To),
NarrowingConversion);
return reinterpret_cast<To>(from);
}
int main() {
void* p = 0;
int i = safe_cast<int>(p);
cout << "int cast okay\n";
//! char c = safe_cast<char>(p);
} ///:~.
This example defines a function template, safe_cast<>( ), that checks to see if the object it is casting from is no larger than the type of object it casts to. If the size of the target object type is smaller, then the user will be notified at compile time that a narrowing conversion was attempted. Notice that the StaticCheck class template has the curious feature that anything can be converted to an instance of StaticCheck<true> (because of the ellipsis in its constructor[71]), and nothing can be converted to a StaticCheck<false>, because no conversions are supplied for that specialization. The idea is to attempt to create an instance of a new class and attempt to convert it to StaticCheck<true> at compile time whenever the condition of interest is true, or to a StaticCheck<false> object when the condition being tested is false. Since the sizeof operator does its work at compile time, it is used to attempt the conversion. If the condition is false, the compiler will complain that it doesn’t know how to convert from the new class type to StaticCheck<false>. (The extra parentheses inside the sizeof invocation in STATIC_CHECK( ) are to prevent the compiler from thinking that we’re trying to invoke sizeof on a function, which is illegal.) To get some meaningful information inserted into the error message, the new class name carries key text in its name.
The best way to understand this technique is to walk through a specific case. Consider the line in main( ) above which reads:
int i = safe_cast<int>(p);
The call to safe_cast<int>(p) contains the following macro expansion replacing its first line of code:
{ \
class Error_NarrowingConversion{}; \
sizeof(StaticCheck<sizeof(void*) <= sizeof(int)> \
(Error_NarrowingConversion())); \
}
(Recall that the token-pasting preprocessing operator, ##, concatenates its operand into a single token, so Error_##NarrowingConversion becomes the token Error_NarrowingConversion after preprocessing). The class Error_NarrowingConversion is a local class (meaning that it is declared inside a non-namespace scope) because it is not needed elsewhere in the program. The application of the sizeof operator here attempts to determine the size of an instance of StaticCheck<true> (because sizeof(void*) <= sizeof(int) is true on our platforms), created implicitly from the temporary object returned by the call Error_NarrowingConversion( ). The compiler knows the size of the new class Error_NarrowingConversion (it’s empty), and so the compile-time use of sizeof at the outer level in STATIC_CHECK( ) is valid. Since the conversion from the Error_NarrowingConversion temporary to StaticCheck<true> succeeds, so does this outer application of sizeof, and execution continues.