Practical Meta Programming

1,997 views

Published on

0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,997
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
31
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

Practical Meta Programming

  1. 1. Practical Meta-programming By Reggie Meisler
  2. 2. Topics
  3. 3. How it works in general <ul><li>All based around template specialization and partial template specialization mechanics </li></ul><ul><li>Also based on the fact that we can recursively instantiate template classes with about 500 levels of depth </li></ul><ul><li>Conceptually analogous to functional programming languages </li></ul><ul><ul><li>Can only operate on types and immutable data </li></ul></ul><ul><ul><li>Data is never modified, only transformed </li></ul></ul><ul><ul><li>Iteration through recursion </li></ul></ul>
  4. 4. Template mechanics <ul><li>Template specialization rules are simple </li></ul><ul><li>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 </li></ul>
  5. 5. Template mechanics <ul><li>template < typename T> class MyClass { /*…*/ }; // Full specialization template <> class MyClass< int > { /*…*/ }; // Partial specialization template < typename T> class MyClass<T*> { /*…*/ }; </li></ul>
  6. 6. Template mechanics <ul><li>template < typename T> class MyClass { /*…*/ }; // Full specialization template <> class MyClass< int > { /*…*/ }; // Partial specialization template < typename T> class MyClass<T*> { /*…*/ }; </li></ul>MyClass<float> goes here MyClass<int> goes here MyClass<int*> goes here
  7. 7. Template mechanics <ul><li>This filtering mechanism of specialization and partial specialization is like branching at compile-time </li></ul><ul><li>When combined with recursive template instantiation, we’re able to actually construct all the fundamental components of a programming language </li></ul>
  8. 8. How it works in general <ul><li>// Example of a simple summation template < 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 </li></ul>
  9. 9. How it works in general <ul><li>// Example of a simple summation int mySum = Sum<10>::value; // mySum = 55 = 10 + 9 + 8 + … + 3 + 2 + 1 </li></ul>
  10. 10. How it works in general <ul><li>// Example of a type trait that checks for const template < 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 ; }; </li></ul>
  11. 11. How it works in general <ul><li>// Example of a type trait that checks for const bool amIConst1 = IsConst< const float >::value; bool amIConst2 = IsConst< unsigned >::value; // amIConst1 = true // amIConst2 = false </li></ul>
  12. 12. Type Traits <ul><li>Already have slides on how these work (Go to C++/Architecture club moodle) </li></ul><ul><li>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 </li></ul><ul><li>Later in the slides, we’ll talk about SFINAE , which is considered to be a very powerful type trait </li></ul>
  13. 13. Math <ul><li>Mathematical functions are by definition, functional . Some input is provided, transformed by some operations, then we’re given an output </li></ul><ul><li>This makes math functions a perfect candidate for compile-time precomputation </li></ul>
  14. 14. Fibonacci <ul><li>template < int N> // Fibonacci function struct 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; }; </li></ul>
  15. 15. Fibonacci <ul><li>Now let’s use it! // Print out 42 fib values for ( int i = 0; i < 42; ++i ) printf( “fib(%d) = %d ” , i, Fib<i>::value); </li></ul><ul><li>What’s wrong with this picture? </li></ul>
  16. 16. Real-time vs Compile-time <ul><li>Oh crap! Our function doesn’t work with real-time variables as inputs! </li></ul><ul><li>It’s completely impractical to have a function that takes only literal values </li></ul><ul><li>We might as well just calculate it out and type it in, if that’s the case! </li></ul>
  17. 17. Real-time vs Compile-time <ul><li>Once we create compile-time functions, we need to convert their results into real-time data </li></ul><ul><li>We need to drop all the data into a table (Probably an array for O(1) indexing) </li></ul><ul><li>Then we can access our data in a practical manner (Using real-time variables, etc) </li></ul>
  18. 18. Fibonacci Table <ul><li>int 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() {} }; </li></ul>
  19. 19. Fibonacci Table <ul><li>Now our Fibonacci numbers can scale based on the value of MAX_FIB_VALUE , without any extra code </li></ul><ul><li>To build the table we can just start the template recursion like so: FillFibTable<>::Do(); </li></ul><ul><li>The template recursion should compile into code equivalent to: FibTable[0] = 1; FibTable[1] = 1; // etc… until MAX_FIB_VALUE </li></ul>
  20. 20. Using Fibonacci <ul><li>// Print out 42 fib values for ( int i = 0; i < 42; ++i ) printf( “fib(%d) = %d ” , i, FibTable[i]); // Output: // fib(0) = 1 // fib(1) = 1 // fib(2) = 2 // fib(3) = 3 // … </li></ul>
  21. 21. The Meta Tradeoff <ul><li>Now we can quite literally see the tradeoff for meta-programming’s magical O(1) execution time </li></ul><ul><li>A classic memory vs speed problem </li></ul><ul><ul><li>Meta, of course, favors speed over memory </li></ul></ul><ul><ul><li>Which is more important for your situation? </li></ul></ul>
  22. 22. Compile-time recursive function calls <ul><li>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 </li></ul><ul><li>As you’ll see, this increases the flexibility of your code while giving you near-hard-coded performance </li></ul>
  23. 23. Dot Product <ul><li>template < 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 prod template < typename T> struct DotProd<T, 1> { static T Do( const T* a, const T* b) { return (*a) * (*b); } }; </li></ul>
  24. 24. Dot Product <ul><li>// Syntactic sugar template < typename T, int Dim> T DotProduct(T (&a)[Dim], T (&b)[Dim]) { return DotProd<T, Dim>::Do(a, b); } // Example use float v1[3] = { 1.0f, 2.0f, 3.0f }; float v2[3] = { 4.0f, 5.0f, 6.0f }; DotProduct(v1, v2); // = 32.0f </li></ul>Always take advantage of auto-type detection!
  25. 25. Dot Product <ul><li>// Other possible method, assuming POD vector // * Probably more practical template < 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); } </li></ul>
  26. 26. Dot Product <ul><li>// Other possible method, assuming POD vector // * Probably more practical template < 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); } </li></ul>We can auto-determine the dimension based on size since T is a POD vector
  27. 27. Approximating Sine <ul><li>Sine is a function we’d usually like to approximate for speed reasons </li></ul><ul><li>Unfortunately, we’ll only get exact values on a degree-by-degree basis </li></ul><ul><ul><li>Because sine technically works on an uncountable set of numbers (Real Numbers) </li></ul></ul>
  28. 28. Approximating Sine <ul><li>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 – x 3 /3! + x 5 /5! – x 7 /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); </li></ul>
  29. 29. Approximating Sine <ul><li>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 – x 3 /3! + x 5 /5! – x 7 /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); </li></ul>Floats can’t be declared inside the template class Need radians for Taylor Series formula Our approximated result
  30. 30. Approximating Sine <ul><li>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 </li></ul><ul><li>Instead of accessing the table for its values directly, we’ll use an interface function </li></ul><ul><li>We can just floor any in-between degree values to index into our table </li></ul>
  31. 31. Final Result: FastSine <ul><li>// Approximates sine by degree float FastSine( float radians) { // Convert to degrees and floor result unsigned degrees = unsigned (radians * 180.0f/PI); // Wrap degrees and index SineTable return SineTable[degrees % 360]; } </li></ul>
  32. 32. Tuples <ul><li>Ever want a heterogeneous container? You’re in luck! A Tuple is simple, elegant, sans polymorphism, and 100% type-safe! </li></ul><ul><li>A Tuple is a static data structure defined recursively by templates </li></ul>
  33. 33. Tuples <ul><li>struct NullType {}; // Empty structure template < typename T, typename U = NullType> struct Tuple { typedef T head; typedef U tail; </li></ul><ul><li> T data; U next; }; </li></ul>
  34. 34. Making a Tuple <ul><li>typedef Tuple< int , Tuple< float , Tuple<MyClass>>> MyType; MyType t; t.data // Element 1 t.next.data // Element 2 t.next.next.data // Element 3 </li></ul>This is what I mean by “recursively defined”
  35. 35. 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
  36. 36. Tuple<MyClass> Tuple<float, Tuple<MyClass>> Tuple<int, Tuple<float, Tuple<MyClass>>> data: int data: float data: MyClass NullType next next next
  37. 37. Better creation <ul><li>template < 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… </li></ul>Not the best solution, but simplifies syntax
  38. 38. Making a Tuple Pt 2 <ul><li>typedef MakeTuple< int , float , MyClass> MyType; MyType t; t.data // Element 1 t.next.data // Element 2 t.next.next.data // Element 3 </li></ul>But can we do something about this indexing mess? Better
  39. 39. Better indexing <ul><li>template < 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; } }; </li></ul>It’s a good thing we made those typedefs Making use of template function auto-type detection again
  40. 40. Better indexing <ul><li>// Just to sugar up the syntax a bit #define TGet(list, index) GetValue<index>::From(list) </li></ul>
  41. 41. Delicious Tuple <ul><li>MakeTuple< int , float , MyClass> t; // TGet works for both access and mutation TGet(t, 0) // Element 1 TGet(t, 1) // Element 2 TGet(t, 2) // Element 3 </li></ul>
  42. 42. Tuple <ul><li>There are many more things you can do with Tuple, and many more implementations you can try (This is probably the simplest) </li></ul><ul><li>Tuples are both heterogeneous containers, as well as recursively-defined types </li></ul><ul><li>This means there are a lot of potential uses for them </li></ul><ul><ul><li>Consider how this might be used for messaging or serialization systems </li></ul></ul>
  43. 43. SFINAE (Substitution Failure Is Not An Error) <ul><li>What is it? A way for the compiler to deal with this: struct MyType { typedef int type; }; // Overloaded template functions template < 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) } </li></ul>
  44. 44. SFINAE (Substitution Failure Is Not An Error) <ul><li>When dealing with overloaded function resolution, the compiler can silently reject ill-formed function signatures </li></ul><ul><li>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 </li></ul>
  45. 45. Does MyClass have an iterator? <ul><li>// 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 ” , sizeof (fnc<MyClass>(0)) == sizeof (Yes) ? “Yes” : “No” ); } </li></ul>
  46. 46. Nitty Gritty <ul><li>We can use sizeof to inspect the return value of the function without calling it </li></ul><ul><li>We pass the overloaded function 0 (A null ptr to type T) </li></ul><ul><li>If the function signature is not ill-formed with respect to type T, the null ptr will be less implicitly convertible to the ellipses </li></ul>
  47. 47. Nitty Gritty <ul><li>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! </li></ul><ul><li>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 </li></ul>
  48. 48. Check for member function <ul><li>// 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(…); </li></ul>
  49. 49. Nitty Gritty <ul><li>This means we can silently inspect any public member of a given type at compile-time! </li></ul><ul><li>For anyone who was disappointed about C++0x dropping concepts, they still have a potential implementation in C++ through SFINAE </li></ul>
  50. 50. Questions?

×