practical meta-programming
DESCRIPTION
Practical Meta-programming. By Reggie Meisler. Topics. How it works in general Useful practices Type Traits (Already have slides on that) Math Functions (Fibonacci, Dot Prod, Sine) Static Type Ids Tuples SFINAE. How it works in general. - PowerPoint PPT PresentationTRANSCRIPT
Practical Meta-programming
By Reggie Meisler
Topics
How it works in general
• All based around template specialization and partial template specialization mechanics
• Also based on the fact that we can recursively instantiate template classes with about 500 levels of depth
• Conceptually analogous to functional programming languages– Can only operate on types and immutable data– Data is never modified, only transformed– Iteration through recursion
Template mechanics
• Template specialization rules are simple
• When you specialize a template class, that specialization now acts as a higher-priority filter for any types (or integral values) that attempt to instantiate the template class
Template mechanics
template <typename T>class MyClass { /*…*/ };
// Full specializationtemplate <>class MyClass<int> { /*…*/ };
// Partial specializationtemplate <typename T>class MyClass<T*> { /*…*/ };
Template mechanics
template <typename T>class MyClass { /*…*/ };
// Full specializationtemplate <>class MyClass<int> { /*…*/ };
// Partial specializationtemplate <typename T>class MyClass<T*> { /*…*/ };
MyClass<float> goes here
MyClass<int> goes here
MyClass<int*> goes here
Template mechanics
• This filtering mechanism of specialization and partial specialization is like branching at compile-time
• When combined with recursive template instantiation, we’re able to actually construct all the fundamental components of a programming language
How it works in general// Example of a simple summationtemplate <int N>struct Sum{ // Recursive call! static const int value = N + Sum<N-1>::value;};// Specialize a base case to end recursion!template <>struct Sum<1>{ static const int value = 1;};
// Equivalent to ∑(i=1 to N) i
How it works in general
// Example of a simple summationint mySum = Sum<10>::value;
// mySum = 55 = 10 + 9 + 8 + … + 3 + 2 + 1
How it works in general// Example of a type trait that checks for consttemplate <typename T>struct IsConst{ static const bool value = false;};
// Partially specialize for <const T>template <typename T>struct IsConst<const T>{ static const bool value = true;};
How it works in general
// Example of a type trait that checks for constbool amIConst1 = IsConst<const float>::value;bool amIConst2 = IsConst<unsigned>::value;
// amIConst1 = true// amIConst2 = false
Type Traits
• Already have slides on how these work(Go to C++/Architecture club moodle)
• Similar to IsConst example, but also allows for type transformations that remove or add qualifiers to a type, and deeper type introspection like checking if one type inherits from another
• Later in the slides, we’ll talk about SFINAE, which is considered to be a very powerful type trait
Math
• Mathematical functions are by definition, functional. Some input is provided, transformed by some operations, then we’re given an output
• This makes math functions a perfect candidate for compile-time precomputation
Fibonaccitemplate <int N> // Fibonacci functionstruct Fib{ static const int value = Fib<N-1>::value + Fib<N-2>::value;};
template <>struct Fib<0> // Base case: Fib(0) = 1{ static const int value = 1;};
template <>struct Fib<1> // Base case: Fib(1) = 1{ static const int value = 1;};
Fibonacci
• Now let’s use it!
// Print out 42 fib valuesfor( int i = 0; i < 42; ++i ) printf(“fib(%d) = %d\n”, i, Fib<i>::value);
• What’s wrong with this picture?
Real-time vs Compile-time
• Oh crap! Our function doesn’t work with real-time variables as inputs!
• It’s completely impractical to have a function that takes only literal values
• We might as well just calculate it out and type it in, if that’s the case!
Real-time vs Compile-time
• Once we create compile-time functions, we need to convert their results into real-time data
• We need to drop all the data into a table (Probably an array for O(1) indexing)
• Then we can access our data in a practical manner (Using real-time variables, etc)
Fibonacci Tableint FibTable[ MAX_FIB_VALUE ]; // Our table
template <int index = 0>struct FillFibTable{ static void Do() { FibTable[index] = Fib<index>::value; FillFibTable<index + 1>::Do(); // Recursive loop, unwinds at compile-time }};
// Base case, ends recursion at MAX_FIB_VALUE template <>struct FillFibTable<MAX_FIB_VALUE>{ static void Do() {}};
Fibonacci Table• Now our Fibonacci numbers can scale based on the value of
MAX_FIB_VALUE, without any extra code
• To build the table we can just start the template recursion like so:
FillFibTable<>::Do();
• The template recursion should compile into code equivalent to:
FibTable[0] = 1;FibTable[1] = 1; // etc… until MAX_FIB_VALUE
Using Fibonacci
// Print out 42 fib valuesfor( int i = 0; i < 42; ++i ) printf(“fib(%d) = %d\n”, i, FibTable[i]);
// Output:// fib(0) = 1// fib(1) = 1// fib(2) = 2// fib(3) = 3// …
The Meta Tradeoff
• Now we can quite literally see the tradeoff for meta-programming’s magical O(1) execution time
• A classic memory vs speed problem– Meta, of course, favors speed over memory– Which is more important for your situation?
Compile-time recursive function calls
• Similar to how we unrolled our loop for filling the Fibonacci table, we can unroll other loops that are usually placed in mathematical calculations to reduce code size and complexity
• As you’ll see, this increases the flexibility of your code while giving you near-hard-coded performance
Dot Producttemplate <typename T, int Dim>struct DotProd{ static T Do(const T* a, const T* b) { // Recurse (Ideally unwraps to the hard-coded equivalent in assembly) return (*a) * (*b) + DotProd<T, Dim – 1>::Do(a + 1, b + 1); }};
// Base case: end recursion at single element vector dot prodtemplate <typename T>struct DotProd<T, 1>{ static T Do(const T* a, const T* b) { return (*a) * (*b); }};
Dot Product
// Syntactic sugartemplate <typename T, int Dim>T DotProduct(T (&a)[Dim], T (&b)[Dim]){ return DotProd<T, Dim>::Do(a, b);}
// Example usefloat v1[3] = { 1.0f, 2.0f, 3.0f };float v2[3] = { 4.0f, 5.0f, 6.0f };
DotProduct(v1, v2); // = 32.0f
Always take advantage of
auto-type detection!
Dot Product
// Other possible method, assuming POD vector// * Probably more practicaltemplate <typename T>float DotProduct(const T& a, const T& b){ static const size_t Dim = sizeof(T)/sizeof(float);
return DotProd<float, Dim>::Do((float*)&a, (float*)&b);}
Dot Product
// Other possible method, assuming POD vector// * Probably more practicaltemplate <typename T>float DotProduct(const T& a, const T& b){ static const size_t Dim = sizeof(T)/sizeof(float);
return DotProd<float, Dim>::Do((float*)&a, (float*)&b);}
We can auto-determine the dimension based on size since T is a POD vector
Approximating Sine
• Sine is a function we’d usually like to approximate for speed reasons
• Unfortunately, we’ll only get exact values on a degree-by-degree basis– Because sine technically works on an uncountable
set of numbers (Real Numbers)
Approximating Sinetemplate <int degrees>struct Sine{ static const float radians; static const float value;};
template <int degrees>const float Sine<degrees>::radians = degrees*PI/180.0f;
// x – x3/3! + x5/5! – x7/7! (A very good approx)template <int degrees>const float Sine<degrees>::value =radians - ((radians*radians*radians)/6.0f) + ((radians*radians*radians*radians*radians)/120.0f) –((radians*radians*radians*radians*radians*radians*radians)/5040.0f);
Approximating Sinetemplate <int degrees>struct Sine{ static const float radians; static const float value;};
template <int degrees>const float Sine<degrees>::radians = degrees*PI/180.0f;
// x – x3/3! + x5/5! – x7/7! (A very good approx)template <int degrees>const float Sine<degrees>::value =radians - ((radians*radians*radians)/6.0f) + ((radians*radians*radians*radians*radians)/120.0f) –((radians*radians*radians*radians*radians*radians*radians)/5040.0f);
Floats can’t be declared inside the template class
Need radians for Taylor Series formula
Our approximated result
Approximating Sine
• We’ll use the same technique as shown with the Fibonacci meta function for generating a real-time data table of Sine values from 0-359 degrees
• Instead of accessing the table for its values directly, we’ll use an interface function
• We can just interpolate any in-between degree values using our table constants
Final Result: FastSine
// Approximates sine, favors ceil valuefloat FastSine(float radians){ // Convert to degrees float degrees = radians * 180.0f/PI; unsigned approxA = (unsigned)degrees; unsigned approxB = (unsigned)ceil(degrees); float t = degrees - approxA; // Wrap degrees, use linear interp and index SineTable return t * SineTable[approxB % 360] + (1-t) * SineTable[approxA % 360];}
Tuples
• Ever want a heterogeneous container? You’re in luck! A Tuple is simple, elegant, sans polymorphism, and 100% type-safe!
• A Tuple is a static data structure defined recursively by templates
Tuples
struct NullType {}; // Empty structure
template <typename T, typename U = NullType>struct Tuple{ typedef T head; typedef U tail; T data; U next;};
Making a Tuple
typedef Tuple<int, Tuple<float, Tuple<MyClass>>> MyType;
MyType t;
t.data // Element 1t.next.data // Element 2t.next.next.data // Element 3
This is what I mean by “recursively defined”
Tuple<int, Tuple<float, Tuple<MyClass>>>
Tuple in memory
data: int
next: Tuple<float, Tuple<MyClass>>
data: float
next: Tuple<MyClass>
data: MyClass
next: NullType
Tuple<MyClass>
Tuple<float, Tuple<MyClass>>
Tuple<int, Tuple<float, Tuple<MyClass>>>
data: int
data: float
data: MyClass
NullType
next
next
next
Better creationtemplate <typename T1 = NullType, typename T2 = NullType, …>struct MakeTuple;
template <typename T1>struct MakeTuple<T1, NullType, …> // Tuple of one type{ typedef Tuple<T1> type;};
template <typename T1, typename T2>struct MakeTuple<T1, T2, …> // Tuple of two types{ typedef Tuple<T1, Tuple<T2>> type;};
// Etc…
Not the best solution, but simplifies syntax
Making a Tuple Pt 2
typedef MakeTuple<int, float, MyClass> MyType;
MyType t;
t.data // Element 1t.next.data // Element 2t.next.next.data // Element 3
But can we do something about this
indexing mess?
Better
Better indexingtemplate <int index>struct GetValue{ template <typename TList> static typename TList::head& From(TList& list) { return GetValue<index-1>::From(list.next); // Recurse }};
template <>struct GetValue<0> // Base case: Found the list data{ template <typename TList> static typename TList::head& From(TList& list) { return list.data; }};
It’s a good thing we made those typedefs
Making use of template function
auto-type detection again
Better indexing
// Just to sugar up the syntax a bit#define TGet(list, index) \
GetValue<index>::From(list)
Delicious Tuple
MakeTuple<int, float, MyClass> t;
// TGet works for both access and mutationTGet(t, 0) // Element 1TGet(t, 1) // Element 2TGet(t, 2) // Element 3
Tuple
• There are many more things you can do with Tuple, and many more implementations you can try (This is probably the simplest)
• Tuples are both heterogeneous containers, as well as recursively-defined types
• This means there are a lot of potential uses for them• Consider how this might be used for messaging or
serialization systems
SFINAE(Substitution Failure Is Not An Error)
• What is it? A way for the compiler to deal with this:
struct MyType { typedef int type; };
// Overloaded template functionstemplate <typename T>void fnc(T arg);
template <typename T>void fnc(typename T::type arg);
void main(){ fnc<MyType>(0); // Calls the second fnc fnc<int>(0); // Calls the first fnc (No error)}
SFINAE(Substitution Failure Is Not An Error)• When dealing with overloaded function
resolution, the compiler can silently rejectill-formed function signatures
• As we saw in the previous slide, int was ill-formed when matched with the function signature containing, typename T::type, but this did not cause an error
Does MyClass have an iterator?// Define types of different sizes typedef long Yes;typedef short No;
template <typename T>Yes fnc(typename T::iterator*); // Must be pointer!
template <typename T>No fnc(…); // Lowest priority signature
void main(){ // Sizeof check, can observe types without calling fnc printf(“Does MyClass have an iterator? %s \n”, sizeof(fnc<MyClass>(0)) == sizeof(Yes) ? “Yes” : “No”);}
Nitty Gritty
• We can use sizeof to inspect the return value of the function without calling it
• We pass the overloaded function 0(A null ptr to type T)
• If the function signature is not ill-formed with respect to type T, the null ptr will be less implicitly convertible to the ellipses
Nitty Gritty
• Ellipses are SO low-priority in terms of function overload resolution, that any function that even stands a chance of working (is not ill-formed) will be chosen instead!
• So if we want to check the existence of something on a given type, all we need to do is figure out whether or not the compiler chose the ellipses function
Check for member function// Same deal as before, but now requires this struct// (Yep, member function pointers can be template// parameters)template <typename T, T& (T::*)(const T&)>struct SFINAE_Helper;
// Does my class have a * operator?// (Once again, checking w/ pointer)template <typename T>Yes fnc(SFINAE_Helper<T, &T::operator*>*);
template <typename T>No fnc(…);
Nitty Gritty
• This means we can silently inspect any public member of a given type at compile-time!
• For anyone who was disappointed about C++0x dropping concepts, they still have a potential implementation in C++ through SFINAE
Questions?