520 likes | 730 Views
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.
E N D
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 • 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>classMyClass { /*…*/ }; // Full specializationtemplate <>classMyClass<int> { /*…*/ };// Partial specializationtemplate <typename T>classMyClass<T*> { /*…*/ };
Template mechanics MyClass<float> goes here template <typename T>classMyClass { /*…*/ }; // Full specializationtemplate <>classMyClass<int> { /*…*/ };// Partial specializationtemplate <typename T>classMyClass<T*> { /*…*/ }; 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 intvalue = N + Sum<N-1>::value;};// Specialize a base case to end recursion!template <>struct Sum<1>{static const intvalue = 1;};// Equivalent to ∑(i=1 to N)i
How it works in general // Example of a simple summationintmySum = 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>structIsConst{static const boolvalue = false;};// Partially specialize for <const T>template <typename T>structIsConst<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
Fibonacci template <intN> // 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( inti = 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 Table intFibTable[ MAX_FIB_VALUE ]; // Our tabletemplate <int index = 0>structFillFibTable{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 <>structFillFibTable<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( inti = 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 Product template <typenameT, int Dim>structDotProd{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>structDotProd<T, 1>{static T Do(const T* a, const T* b) {return (*a) * (*b); }};
Dot Product Always take advantage ofauto-type detection! // 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
Dot Product // Other possible method, assuming POD vector// * Probably more practicaltemplate <typename T>floatDotProduct(const T& a, const T& b){static const size_tDim = 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>floatDotProduct(const T& a, const T& b){static const size_tDim = 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 Sine template <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>constfloat 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 Sine Floats can’t be declared inside the template class template <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>constfloat 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); 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 valuefloatFastSine(float radians){ // Convert to degreesfloat 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 SineTablereturn 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 structNullType {}; // Empty structuretemplate <typename T, typename U = NullType>structTuple{typedefT head;typedefU tail; T data;U next;};
Making a Tuple This is what I mean by “recursively defined” typedefTuple<int,Tuple<float,Tuple<MyClass>>>MyType;MyType t;t.data// Element 1t.next.data// Element 2t.next.next.data// Element 3
Tuple in memory Tuple<int, Tuple<float, Tuple<MyClass>>> data:int next: Tuple<float, Tuple<MyClass>> data: float next: Tuple<MyClass> data: MyClass next: NullType
Tuple<int, Tuple<float, Tuple<MyClass>>> data:int next Tuple<float, Tuple<MyClass>> data: float next Tuple<MyClass> data: MyClass next NullType
Better creation template <typename T1 = NullType, typename T2 = NullType, …>structMakeTuple;template <typename T1>structMakeTuple<T1, NullType, …> // Tuple of one type{typedefTuple<T1> type;};template <typename T1, typename T2>structMakeTuple<T1, T2, …> // Tuple of two types{typedefTuple<T1, Tuple<T2>> type;};// Etc… Not the best solution, but simplifies syntax
Making a Tuple Pt 2 typedefMakeTuple<int, float, MyClass>MyType;MyType t;t.data// Element 1t.next.data// Element 2t.next.next.data// Element 3 Better But can we do something about this indexing mess?
Better indexing It’s a good thing we made those typedefs template <int index>structGetValue{template <typenameTList>statictypenameTList::head& From(TList& list) {returnGetValue<index-1>::From(list.next); // Recurse }};template <>structGetValue<0> // Base case: Found the list data{template <typenameTList>statictypenameTList::head& From(TList& list) { return list.data; }}; Making use of template function auto-type detection again
Better indexing // Just to sugar up the syntax a bit#defineTGet(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:structMyType { typedefinttype; };// Overloaded template functionstemplate <typename T>voidfnc(T arg);template <typename T>voidfnc(typenameT::type arg);void main(){fnc<MyType>(0); // Calls the second fncfnc<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, intwas ill-formed when matched with the function signature containing, typenameT::type, but this did not cause an error
Does MyClass have an iterator? // Define types of different sizes typedeflong Yes;typedefshort No;template <typename T>Yes fnc(typename T::iterator*); // Must be pointer!template <typename T>No fnc(…); // Lowest priority signaturevoid main(){// Sizeof check, can observe types without calling fncprintf(“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&)>structSFINAE_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