More resources

Short version

Each lambda term folds its definition into its type, e.g, lambda (x, x * x) has the type Sum < Arg<A>, Arg<A> > , where A is the type of the meta-variable x . Thus, everwhere the lambda term is applied, there is enough information for a template meta-program to unfold the type back into the body of the function.

Breaking it down

To understand how this works, let's break down an example:

integrate(lambda(x, x * x), 0.0, 1.0, 10000) ;

Passing anonymous functions as types

The prototype for the integrate function is:

template <typename F> static inline double integrate (F f, double a, double b, int n) ;

At this point, an experienced C++ progarmmer might be puzzled: the type of the function parameter ( F ) seems to be unrestricted. In practice, the type we pass for this parameter will encode all of the information necessary to reconstruct the function we wish to invoke. Since templates are expanded lazily, if we pass a type that doesn't encode a function, we'll get a compile-time error when it tries to inline it.

Calling anonymous functions

Peeking inside the function integrate , we get a glimpse at how this is done; instead of a call to f(x) , we see:

... Inline<F>::at(x) ...

f

In fact, the compiler warns that the run-time parameterisn't even used! (This is a good indication that whatever is happening, it's happening at compile-time.)

So, clearly, Inline<F> is a template meta-program that reconstructs an expression equivalent to the function described by F , and the at static member function inlines this expression, with its argument in place of the function's argument.

Constructing anonymous functions

To construct an anonymous function, we need to encode its definition as a type. In compiler theory, it's standard to encode an expression as a syntax tree. So, we just need a way to encode syntax trees as types.

For now, we'll assume all anonymous functions take only one argument (this method generalizes to multiple arguments), and that it contains no free variables (this method also generalizes to free variables).

We'll use the base class Exp<T> to represent an expression with a syntax tree encoded as type T :

template <typename T> struct Exp { template <typename M> Prod<T,M> operator * (M m) ; template <typename M> Sum<T,M> operator + (M m) ; } ;

Note how the nested template member operator overloads for addition and multiplication preserve the structure of the computation in the resulting type. So, if we have two expressions e1 and e2 whose types represent compile-time expressions, then the type of e1 + e1 represents a compile-time expression that computes their sum.

For our very simple demonstration language, we'll restrict the domain-specific embedded language that describes anonymous functions to just arguments, sums, products and constants. If you want do anything serious with this technique, you'll probably want to enrich this language with more expression types first:

// Integer constant: template <int i> struct I : public Exp<I<i> > { static I<i> v ; } ; // Function argument of type T: template <typename T> struct Arg : public Exp< Arg<T> > {} ; // Sum of two expressions: template <typename R1, typename R2> struct Sum : public Exp<Sum<R1,R2> > {} ; // Product of two expressions: template <typename R1, typename R2> struct Prod : public Exp<Prod<R1,R2> > {} ;

Clearly, we don't need to define any member functions, since we'll never actually be using instances of these types. These types are compile-time data structures for a template meta-program. Note that the base class of these expressions contains their own type as an argument. This technique is what lets the overloaded operations in the base class work.

Putting it all together, we can create a compile-time anonymous function that squares its argument like so:

Arg<double> x ; // x is now a meta-parameter. ... lambda(x, x * x) ... // has type Prod<Arg<double>,Arg<double> >

lambda

template <typename T, typename A> static inline T lambda (A arg, T body) { (void) arg ; (void) body ; return Dummy<T>::value ; }

and, thefunction is just a syntactic placebo; for single-argument anonymous functions, its role is aesthetic:

(If you allowed for multi-argument anonymous functions, the lambda would actually be necessary.)

Inlining anonymous functions

To unfold the definition of the function at its point of use requires a very basic C++ template meta-program. C++ template meta-programs are, for the most part, just purely functional pattern-matching and term-rewriting systems.

Assuming that each compile-time anonymous function F is invoked as Inline<F>::at(x) , the following template meta-program inlines the function encoded in type F and then applies it to x ;

template <typename E> struct Inline { } ; // Inline constants: template < int i > struct Inline< I<i> > { static inline int at (int arg) { (void)arg ; return i ; } } ; // Inline arguments: template < typename X1 > struct Inline<Arg<X1> > { static inline X1 at (X1 arg) { return arg ; } } ; // Inline sums: template < typename E1, typename E2 > struct Inline<Sum<E1,E2> > { template <typename A> static inline A at (A arg) { return Inline<E1>::at(arg) + Inline<E2>::at(arg) ; } } ; // Inline products: template < typename E1, typename E2 > struct Inline<Prod<E1,E2> > { template <typename A> static inline A at (A arg) { return Inline<E1>::at(arg) * Inline<E2>::at(arg) ; } } ;

Compiler notes

A key assumption in this demonstration is that the C++ compiler will obey the inline keyword. Of course, C++ compilers are not required to do this, so it's important to understand when they will. In my own tests, g++ started inlining functions marked static inline at optimization levels -O2 and higher.

Glancing at the the x86 code generated for

double area = integrate(lambda(x, x * x), 0.0, 1.0, 10000) ; printf("integrate: %f

", area) ;

main

L11: movapd %xmm1, %xmm0 mulsd %xmm1, %xmm0 mulsd %xmm2, %xmm0 addsd %xmm0, %xmm3 incl %eax addsd %xmm2, %xmm1 cmpl $10000, %eax jne L11 movapd %xmm3, %xmm0 leaq LC2(%rip), %rdi movl $1, %eax call _printf

printf

main

Code

at -O2, I found this sitting insideAs you can see, the only call is to, and the loop got inlined in

Download the full demo code.

As an exercise, you could extend it with a richer set of expressions in the DSEL. A more exciting upgrade would be to thread the values of free variables through the run-time value of the anonymous function; at present, this is just is a dummy-value that gets eliminated by the optimizer. Then, you'd have self-inlining anonymous closures!