Practical Meta-programming
description
Transcript of Practical Meta-programming
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?