Practical Meta-programming

Post on 21-Jan-2016

46 views 1 download

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 Presentation

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?