Download as pdf or txt
Download as pdf or txt
You are on page 1of 12

The Lambda Library: Lambda Abstraction in C++

Jaakko Järvi Gary Powell


Turku Centre for Computer Science (TUCS) Sierra On-Line Ltd.
Lemminkäisenkatu 14-18 A gwpowell@hotmail.com
FIN-20520 Turku, Finland
jaakko.jarvi@cs.utu.fi

Abstract and goes over the implementation details [4].

The Lambda Library (LL) adds a form of lambda ab-


straction to C++. The LL is implemented as a tem- 1.1 Motivation
plate library using standard C++; thus no language
extensions or preprocessing is required. The LL con- Typically STL algorithms operate on container ele-
sists of a rich set of tools for defining unnamed func- ments via functions and function objects passed as
tions. Particularly, these unnamed functions work arguments to the algorithms. The STL contains pre-
seamlessly with the STL algorithms. The LL offers defined function objects for some common cases (such
significant improvements, in terms of generality and as plus, less and negate). In addition, it contains
ease of use, compared to the binders and functors in adaptors for creating function objects from function
the C++ standard library. pointers etc. Further, there are binder templates
bind1st and bind2nd for creating unary function ob-
jects from binary function objects by binding one of
1 Introduction the arguments to a constant value. Some STL imple-
mentations contain function composition operations
The ability to define lambda functions, i.e. unnamed as extensions to the standard [5].
functions, is a standard feature in functional pro- The goal of all these tools is clear: to make it possi-
gramming languages. For some reason, this feature ble to specify unnamed functions in a call to an STL
does not appear in mainstream procedural or object- algorithm. However, this set of tools leaves much
oriented languages (except for Eiffel, where agents [1], room for improvement. Unnamed functors built as
a recently added language feature provides means to compositions of standard function objects, binders,
define unnamed functions). In this paper we intro- adaptors etc. are very hard to read in all but the
duce the Lambda Library which fixes this ’omission’ simplest cases. Moreover, the ’lambda abstraction’
for C++. with the standard tools is full of restrictions. For ex-
The Lambda Library (LL) is a C++ template li- ample, the standard binders allow only one argument
brary implementing a form of lambda abstraction for of a binary function to be bound; there are no binders
C++. The library is designed to work with the Stan- for 3-ary, 4-ary etc. functions. See [6, 7] for a more
dard Template Library (STL) [2], now part of the in-depth discussions about these restrictions.
C++ Standard Library [3]. This article introduces The Lambda Library solves these problems. The
the features of the library rather than explains its syntax is intuitive and there are no (well, fewer) arbi-
inner workings. There is an accompanying technical trary restrictions. The concrete consequences of the
report, which describes the features more thoroughly LL on the tools in the Standard Library are:

1
• The standard functors plus, minus, less etc. The operator- replaces the call to negate, bind
become unnecessary. Instead, the corresponding function replaces the compose1 call, ptr_fun be-
operators are used directly. comes unnecessary, along with bind2nd, and the call
to multiplies is replaced with the operator*. The
• The binders bind1st and bind2nd are replaced argument _1 is a placeholder representing the param-
by a more general bind function template. Us- eter of the function object. At each iteration, it will
ing bind, arbitrary arguments of practically any be substituted by the element from the container of
C++ function can be bound. Furthermore, angles.
bind makes ptr_fun, mem_fun and mem_fun_ref
adaptors unnecessary.
1.2 Relation to other work
• No explicit function composition operators are
The LL combines, extends and generalizes the func-
needed.
tionalities of the Expression Template library (ET)
We’ll take an example to demonstrate what LL’s by Powell and Higley [8] and the Binder Library by
impact can be on code using STL algorithms. The Järvi [6]. Other related work includes the FACT [9]
following example is an extract from the documenta- and FC++ [10] libraries, both developed coevally
tion of one STL implementation: with the LL. The basic idea behind the FACT lambda
functions is quite similar to the LL counterparts, al-
... calculates the negative of the sines of the elements though the syntax is different. Compared to LL,
in a vector, where the elements are angles measured in FACT supports a smaller set of operators. FACT
degrees. Since the C library function sin takes its ar- deliberately allows no side effects in lambda func-
guments in radians, this operation is the composition tions, which means that, e.g. various assignment op-
of three operations: negation, sin, and the conversion erators are not supported. FACT lambda functions
of degrees to radians. are ’expression template aware’ (see [11]), while basic
LL lambda functions are not. Interaction with other
vector<double> angles; expression template libraries deserves some more re-
vector<double> sines; search. Further, FACT provides other features in ad-
const double pi = 3.14159265358979323846; dition to lambda functions, such as lazy lists.
... The FC++ is another library adding functional
assert(sines.size() >= angles.size()); features to C++; it more or less embeds a functional
transform(angles.begin(), angles.end(), sublanguage to C++. A notable feature of FC++ is
sines.begin(), the possibility to define variables for storing lambda
compose1( functions. This is very convenient, even though the
negate<double>(), feature comes with some cost: the lambda function
compose1( becomes dynamically bound, and the return type and
ptr_fun(sin), the argument types of the lambda function must be
bind2nd(multiplies<double>(), defined explicitly by the client. The Function Library
pi / 180.0)))); in C++ Boost [12] has a similar feature in a form that
can be combined with other libraries, e.g. with the
LL.
Using LL constructs, the transform function call can To our understanding, FACT and FC++ take the
be written as: functional features in a ’pure’ form. The LL imple-
ments lambda functions, but does some adjustments
transform(angles.begin(), angles.end(), for a better fit to C++. Our foremost goal is to pro-
sines.begin(), vide lambda functions which match perfectly with the
- bind(sin, _1 * pi / 180.0)); STL style of programming.

2
2 Basic usage In this call to sort, we are sorting the elements by
their contents in descending order. Note that the
This section describes the basic rules for writing lambda expression *_1 > *_2 contains two different
lambda expressions consisting of operator invocations placeholders, _1 and _2. Consequently, it creates a
and different types of functions and function-like con- binary lambda functor. When this functor is called,
structs. Note that there are exceptions to these basic the first argument is substituted for _1 and the sec-
rules. Some operators in C++ have special seman- ond argument for _2. Finally we’ll output the sorted
tics, which is reflected in the semantics of the corre- content of vp separated by line breaks:
sponding lambda expressions as well. We cover these
special cases in section 3. for_each(vp.begin(), vp.end(),
cout << *_1 << endl);
2.1 Introductory examples
2.2 Placeholders
It is easiest to understand how to use the LL by look-
ing at some examples. So let’s start with some simple In lambda functions occurring in lambda calculus and
expressions and work up. First, we’ll initialize the el- in functional programming languages the formal pa-
ements of a container (e.g. a list) to the value 1: rameters are commonly named within the lambda ex-
pression with some explicit syntax. For example, the
list<int> v(10); following definition gives the names x and y to the
for_each(v.begin(), v.end(), _1 = 1); formal parameters of an addition function:
In this example _1 = 1 creates a lambda function λxy.x + y
which assigns the value 1 to every element in v; The
variable _1 is a placeholder, an empty slot, which will The LL counterpart of the above expression is written
be filled with a value at each iteration. Regarding as:
terminology, we call _1 = 1 a lambda expression. A
function object created by a lambda expression is a _1 + _2
lambda functor.
In this version the formal parameters, i.e. the place-
Next, we create a container of pointers and make
holder variables, have predefined names. There is
them point to the elements in the first container v:
no explicit syntactic construct for C++ lambda ex-
list<int*> vp(10); pressions; the use of a placeholder variable in an ex-
transform(v.begin(), v.end(), pression implicitly turns the expression into a lambda
vp.begin(), &_1); expression.1
So far we have used the placeholders _1 and _2.
Here we take the address of each element in v (with The LL supports one more: _3. This means that
&_1) and assign it to the corresponding element in vp. lambda functors can take one, two or three arguments
Now lets change the values in v. For each element we passed in by the STL algorithm; zero parameters is
call some function foo passing the original value of possible too. It would be straightforward to support
the element as an argument to foo: higher arities, but no STL algorithm accepts a func-
tor with the number of arguments greater than two,
int foo(int); so the three placeholders should be enough. In fact,
for_each(v.begin(), v.end(), the third placeholder is a necessity in order to im-
_1 = bind(foo, _1)); plement all the features of the current library (see
section 3.6, which introduces _E, another incarnation
Next we’ll sort the elements of vp:
of _3).
sort(vp.begin(), vp.end(), *_1 > *_2); 1 This doesn’t hold for all expressions, see section 2.3

3
The placeholders are variables defined by the li- serves for this purpose. The syntax of a lambda ex-
brary. The variables themselves are not important pression created with the bind function is:
but rather their types are. The types serve as tags,
bind(target-function, bind-argument-list)
which allow us to spot the placeholders from the argu-
ments stored in a lambda functor, and later replace We use the term bind expression to refer to this
them with the actual arguments that are passed in type of lambda expressions. In a bind expression,
when the lambda functor gets called. The LL pro- the bind-argument-list must be a valid argument
vides typedefs for the placeholder types, so it is easy list for target-function, except that any argument
to define the placeholder names to your own liking. can be replaced with a placeholder, or more gener-
Veldhuizen [13] was the first to introduce the con- ally, with another lambda expression. Where a place-
cept of using placeholders in expression templates. holder is used in place of an actual argument, we say
The LL placeholders are somewhat different from the that the argument is unbound.
original ones. You may have noticed from the previ- The target function can be a pointer to function,
ous examples that we do not specify any types for the a reference to function or a function object. More-
arguments that the placeholders stand for. A place- over, it can be a pointer to a member function or
holder leaves the argument totally open, including even a placeholder, or again more generally, a lambda
the type. This means that the lambda functor can expression. In the last case the result of evalu-
be called with arguments of any type for which the ating the corresponding lambda functor must ob-
underlying function makes sense. viously be a function that can be called with the
Consider again the first example we showed in sec- bind-argument-list after substitutions. Note that
tion 2: we use the term target function with all types of
lambda expressions to denote the underlying oper-
for_each(v.begin(), v.end(), _1 = 1); ation of the lambda expression.

The lambda expression _1 = 1 creates a unary 2.3.1 Function pointers as targets


lambda functor, which can be called with any ob-
ject x, for which x = 1 is a valid expression. The The target function can be a pointer or a reference to
for_each algorithm iterates over a container of inte- a non-member function (or a static member function)
gers, thus the lambda functor is called with an argu- and it can be either bound or unbound. For example,
ment of type int. suppose A, B, C and X are some types:
Since the type of the placeholder argument is left
X foo(A, B, C); A a; B b; C c;
open, the return type of the lambda functor is not
...
known either. The LL has a type deduction system
bind(foo, _1, _2, c)
that figures out the return type when the lambda
bind(&foo, _1, _2, c)
functor is called. It covers operators of built-in types
bind(foo, _1, _1, _1)
and ’well-behaved’ operators of user-defined types;
bind(_1, a, b, c)
and for your user-defined operators with unorthodox
return types, the deduction system is easy to extend. The first bind expression returns a binary lambda
functor. The second bind expression has an equiv-
alent functionality, it just uses a function pointer
2.3 Functions as lambda expressions
instead of a reference. The third bind expression
The use of a placeholder as one of the operands turns demonstrates that a certain placeholder can be used
an operator invocation into a lambda expression im- multiple times in a lambda expression. The argument
plicitly. For ordinary function calls this is not the will be duplicated in each place that the placeholder is
case; an explicit syntactic construct is needed. As used. For this bind expression to make sense, and to
the examples above show, the bind function template compile, the argument to the resulting unary lambda

4
functor must be implicitly convertible to A, B and C. can handle only one return type per function object
The fourth bind expression shows the case where the class. Consequently, all the overloaded function call
target function is left unbound; the resulting lambda operators within one class must have the same return
functor takes one parameter, the function to be called type if you want to be able to use them as target
with the arguments a, b and c. functions (there is a way around this, see the technical
In C++, it is possible to take the address of an report [4]).
overloaded function only if the address is assigned to
or used to initialize a properly typed variable. This 2.3.3 Member functions as targets
means that overloaded functions cannot be used in
bind expressions directly: The form of the bind expression with member func-
tion targets is slightly different. By convention, we
void foo(int); void foo(float); int i; have chosen to declare the bind functions with the
... following format:
bind(&foo, _1)(i); // error
... bind(target-member-function, target-object,
void (*pf1)(int) = &foo; bind-argument-list)
bind(pf1, _1)(i); // ok
... If the first argument is a pointer to a member func-
bind(static_cast<void(*)(int)>(&foo), tion of some class A, the second argument is the target
_1)(i); // ok as well object, that is, an object of type A for which the mem-
ber function is to be called. A bound target object
2.3.2 Function objects as targets can be either a reference or pointer to the object; the
LL supports both cases with the same interface:
Function objects can also be used as target functions.
The return type deduction system requires that the bool A::foo(int); A a; A* ap;
function object class defines the return type of the vector<int> ints;
function call operator as the typedef result_type. ...
This is the convention used with adaptable function // reference is ok:
objects in the STL. For example: find_if(ints.begin(), ints.end(),
bind(&A::foo, a, _1));
class A {
...
// pointer is ok:
public:
find_if(ints.begin(), ints.end(),
typedef B result_type;
bind(&A::foo, ap, _1));
B operator()(X, Y, Z);
}; The functionality is identical in both cases. Similarly,
The above function object can be used as: if the target object is unbound, the resulting functor
can be called both via a pointer or a reference:
A a; X x; Y y; Z z; list<A> la; list<Z> lz;
list<A> refs; list<A*> ptrs;
for_each(lz.begin(), lz.end(), find_if(refs.begin(), refs.end(),
bind(a, x, y, _1)); bind(&A::foo, _1, 1));
for_each(la.begin(), la.end(), find_if(ptrs.begin(), ptrs.end(),
bind(_1, x, y, z)); bind(&A::foo, _1, 1));

The function call operator can be overloaded within Analogously to other types of bind expressions, the
a class. However, the return type deduction system target-member-function can be left open as well.

5
2.4 Operators as lambda expressions wanted to output a space separated list of the ele-
ments in some container a. Our first attempt might
We have overloaded almost every operator for lambda
be:
expressions. Hence, the basic rule is that any operand
of any operator can be replaced with a placeholder, for_each(a.begin(), a.end(),
or with a lambda expression. All the preceding code cout << " " << _1);
examples follow this rule. However, there are some
special cases and restrictions: However, this piece of code outputs a single space,
followed by the elements of a without any delimiters.
• The return types cannot be chosen freely while The subexpression cout << " " is evaluated first,
overloading operators ->, new, delete, new[] and it is not a lambda expression. It merely out-
and delete[]. Consequently, we can’t overload puts a space and returns a reference to cout, rather
them directly for lambda expressions. than creates a lambda functor.
To get the effect we want, the constant " " must
• It is not possible to overload the ., .*, and ?:
be turned into a lambda functor with the constant
operators in C++.
function:
• The assignment and subscript operators must be
for_each(a.begin(), a.end(),
defined as member functions, which creates some
cout << constant(" ") << _1);
asymmetry to lambda expressions. For example:
Now rather than writing to the stream immediately,
int i; the operator<< call with cout and a lambda functor
_1 = 1; // a valid lambda expression builds another lambda functor. This lambda functor
i = _1; // error, no assignment from will be evaluated later at each iteration and we get
// placeholder type to int the desired result.
A delayed variable is simply a lambda functor con-
A workaround for this situation is explained in taining a reference to a regular C++ variable and
section 2.5. is created with the function template var. A call
var(i) turns some variable i into a lambda expres-
• As stated in section 2.2, the return type deduc-
sion. A somewhat artificial, but hopefully illustrative
tion system may not handle all user-defined op-
example is to compute the number of elements in a
erators. For example, the return type of all com-
container using the for_each algorithm:
parison operators is expected to be bool. If this
is not true for some user-defined comparison op- int count = 0;
erator, return type deduction fails. In such cases for_each(a.begin(), a.end(), var(count)++);
the deduction system can either be extended, or
temporarily overridden by explicit type informa- The variable count is delayed. Hence, the expres-
tion (see the technical report [4]). sion count++ is evaluated at each iteration within
the body of the for_each function.
2.5 Delayed constants and variables A delayed variable, or a constant, can be created
outside the lambda expression as well. The template
It is sometimes necessary to turn a variable, or a con- classes var_type and constant_type serve for this
stant, into a lambda functor. We call such lambda purpose. Using var_type the previous example be-
functors delayed variables, or delayed constants, re- comes:
spectively. The need for delayed variables and con-
stants arises when we want to write lambda expres- int count = 0;
sions that are operator invocations, but none of the var_type<int>::type vcount(var(count));
operands is a placeholder. For example, suppose we for_each(a.begin(), a.end(), vcount++);

6
This feature is useful if the same variable appears copies, except for the target object, which is stored
repeatedly in a lambda expression. as a reference.
In section 2.4 we brought up the asymmetry within For all cases, LL provides means for overriding the
lambda assignment and subscript operators. As be- default storing mechanism. For example, any bound
comes clear from the above examples, delaying the argument in a lambda expression can be wrapped
evaluation of a variable with var is a solution to this with a function named ref to state that the lambda
problem: functor should store a reference to the argument. Re-
garding the preceding example, the lambda expres-
int i; sion λx.x + i can be created with the aid of the ref
i = _1; // error function as _1 + ref(i). For an in depth discussion
var(i) = _1; // ok about this issue, see [14].

2.6 About bound arguments


3 Advanced features
Bound arguments of a lambda expression are stored
in the lambda functor object. There are few alterna- Our goal has been to make the LL as complete as
tive ways of doing this, and the choices have conse- possible in the sense that any C++ expression could
quences on whether side effects to the bound argu- be turned into a lambda expression. This section de-
ments are allowed or not. Basically the lambda func- scribes how to use some of the C++ specific operators
tors can store temporary copies of the arguments, or in lambda expressions, how to write control struc-
hold const or non-const references to them. The de- tures as lambda expressions and how to construct and
fault is temporary copies. This means that the value destruct objects in lambda expressions; we even show
of a bound argument is fixed when the lambda func- how to do exception handling in lambda expressions.
tor is created and remains constant during its life-
time. For example, the result of the lambda functor
invocation below is 11, not 20: 3.1 Comma and logical operators
The LL overloads the comma operator for sequenc-
int i = 1; (_1 + i)(i = 10); ing lambda expressions together, as did the ET li-
brary [8]. The character ”;” is reserved by the C++
In other words, the lambda expression _1 + i creates language to mean ’end of statement’. For this library
a lambda function λx.x + 1 rather than λx.x + i. we would like to have it mean, ’end of this lambda
As said, this is the default, and for some expres- expression’. Unfortunately it just isn’t possible, so
sions it makes more sense to store the arguments as we are left with operator,.
references and allow side effects to the arguments. As Since comma is also the separator between func-
an example, consider the lambda expression i += _1. tion arguments, extra parenthesis are sometimes nec-
The obvious intention is that calls to the lambda essary to write syntactically correct lambda expres-
functor affect the value of the variable i, rather than sions:
some temporary copy of it. The LL has this behav-
ior: the left argument of the compound assignment for_each(a.begin(), a.end(),
operators (+=, *=, etc.) are stored as references to (cout << _1 << endl,
non-const. Further, to make the streaming operators clog << _1 << endl));
(<< and >>) work, the stream argument is stored as
a reference. Also, as array types cannot be copied, Here the parenthesis are used to group the two
lambda functors store references to arguments that lambda expressions into one expression, as opposed
are of array types. In lambda functors created with to trying to call the for_each function with four ar-
bind expressions, the default is to store temporary guments.

7
The LL follows the C++ rule for always evaluating for_loop(init, test, increment, body)
the left-hand operand of a comma expression before
Again, the arguments to the for_loop function are
the right-hand operand. In the above example, this
lambda functors.
means that each element of a is guaranteed to be first
Let’s take a concrete example. The following code
written to cout and then to clog.
adds 6 to each element of a two-dimensional array:
Note that the short circuiting rules for the opera-
tors &&, and || are respected as well. For example, int a[5][10]; int i;
the following code sets all negative elements of some for_each(a, a+5,
container a to zero: for_loop(var(i)=0, var(i)<10, ++var(i),
_1[var(i)] += 6));
for_each(a.begin(), a.end(),
_1 < 0 && _1 = 0); Note the use of delayed variables to turn the argu-
ments of for_loop into lambda expressions.
As stated in section 2.5, we can avoid the repeated
3.2 Other special operators wrapping of a variable with var if we create the de-
Since the ?: operator cannot be overloaded, we have layed variable beforehand using the var_type tem-
created the function if_then_else_return to dupli- plate. Using var_type the above example becomes:
cate the ?: operator’s functionality.
int i;
The function call operator is overloaded for place-
var_type<int>::type vi(var(i));
holders only, since other lambda functions already
for_each(a, a+5,
define operator().
for_loop(vi=0, vi<10, ++vi, _1[vi] += 6));
The pointer to member function (operator->*) is
a special case in the sense, that it can refer either to Other loop structures are analogous to for_loop.
a member object or a member function. The return type of all control lambda functors is
void.
3.3 Control lambda expressions
The idea of providing lambda expression variants
3.4 Switch
for control structures originates from the ET Li- The lambda expressions for switch control struc-
brary [8]. The LL implements the control lambda tures, as well as for do_once, are more complex since
expressions of the ET library and adds more. the number of cases may vary. The general form of a
In addition to if_then, if_then_else, for_loop, switch lambda expression is:
while_loop, and do_while_loop, the LL also pro-
vides switch_statement and do_once2 . switch_statement(condition,
case_statement<label >(lambda expression),
Control lambda expressions create lambda functors
case_statement<label >(lambda expression),
that implement the behavior of some control struc-
...
ture. The arguments to these function templates are
default_statement(lambda expression)
lambda functors. For example, the following code
)
outputs all even elements of some container a:
The condition argument must be a lambda expres-
for_each(a.begin(), a.end(), sion that creates a lambda functor with an integral
if_then(_1 % 2 == 0, cout << _1)); return type. The different cases are created with the
case_statement functions, and the optional default
As an example of a loop control lambda expression, case with the default_statement function. The case
the pseudo code definition of for_loop is: labels are given as explicitly specified template argu-
2 A special kind of do while construct, see [4]. ments to case_statement functions and the break

8
statements are implicitly part of each case. For exam- creates a lambda function which wraps the construc-
ple, case_statement<1>(a), where a is some lambda tor call type(args) and returns the resulting object.
functor, generates the code: The complementary function destructor exists as
well. The following example reads integers from two
case 1: containers (x and y), constructs pairs out of them
evaluate lambda functor a; and inserts them into a third container:
break;
vector<pair<int, int> > v;
We have specialized the switch_statement function transform(x.begin(), x.end(), y.begin(),
for up to 9 case statements. back_inserter(v),
constructor<pair<int, int> >(_1, _2));
3.5 Constructors and destructors as
lambda expressions 3.6 Exception handling in lambda ex-
pressions
Operators new and delete can be overloaded, but
their return types are fixed. Particularly, the return The LL allows you to create lambda functors that
types cannot be lambda functors. As in the case of throw and catch exceptions. The form of a lambda
the conditional operator, we have defined function expression for try catch blocks is as follows:
templates to circumvent this restriction. The func-
tion template new_ptr creates a lambda functor that try_catch(
wraps a new invocation, delete_ptr respectively a lambda expression,
lambda functor that wraps a deletion. For example: catch_exception<type>(lambda expression),
catch_exception<type>(lambda expression),
int* a[10]; ...
for_each(a, a+10, _1 = new_ptr<int>()); catch_all(lambda expression)
for_each(a, a+10, delete_ptr(_1)); )

The new_ptr function takes the type of the object The first lambda expression is the try block. Each
to be constructed as an explicitly specified template catch_exception defines a catch block; the type of
argument. Note that new_ptr can take arguments the exception to catch is specified with the explicit
as well. They are passed directly to the construc- template argument. The resulting lambda functors
tor invocation and thus allow calls to constructors catch the exceptions as references. The lambda ex-
which take arguments. The lambda functor created pression within the catch_exception defines the ac-
with delete_ptr first evaluates its argument (which tions to take if the exception is caught.
is a lambda functor as well) and then calls delete The last catch block can alternatively be a call to
on the result of this evaluation. We have also de- catch_exception<type> or to catch_all. We have
fined new_array and delete_array for new[] and used catch_all to mean catch(...), since it is not
delete[]. possible to write catch_exception<...>.
To be able to write constructors as lambda expres- Lambda functors for throwing exceptions are cre-
sions, we have to resort to a set of function templates ated with the unary function throw_exception. The
again. We cannot use bind, since it is not possi- argument to this function is the exception to be
ble to take the address of a constructor. Instead, we thrown, or a lambda functor which creates the ex-
have defined a set of constructor functions which ception to be thrown. A lambda functor for rethrow-
create lambda functors for constructing objects. The ing exceptions is created with the nullary rethrow
lambda expression function.
The figure 1. demonstrates the use of the LL ex-
constructor<type>(args) ception handling tools. The first catch block is for

9
for_each( expression. Illegal use of placeholders is caught by
a.begin(), a.end(), the compiler. The last handler (catch_all) demon-
try_catch( strates rethrowing exceptions.
bind(foo, _1), //foo may throw
catch_exception<foo_ex>( 3.7 Nesting STL algorithms
cout << constant("foo_ex: ")
<< "foo argument = " << _1 This section describes work in progress and the ex-
), act syntax for nesting STL algorithms may change
catch_exception<std::exception>( in the future versions of the library. In section 3.3
cout << constant("std::exception: ") we showed an example using for_loop as the func-
<< bind(&std::exception::what, tion object passed to the for_each algorithm. We’ll
_E), repeat the example here:
throw_exception(
constructor<bar_ex>(_1)) int a[5][10]; int i;
), for_each(a, a+5,
catch_all( for_loop(var(i)=0, var(i)<10, ++var(i),
(cout << constant("Unknown"), _1[var(i)] += 6));
rethrow())
We could do better: the inner loop should really be
)
another for_each invocation. However, for_each is
)
a function template, and thus cannot be passed as a
);
parameter. To circumvent this, we have copied the
interface of all the STL algorithms into a subnames-
pace LL, where the names of the standard algorithms
Figure 1: Throwing and handling exceptions. refer to our own lambda functors, which implement
the functionality of the corresponding algorithms (by
calling the standard function template). Using a
handling exceptions of type foo_ex. Note the use nested for_each, the above example becomes:
of the _1 placeholder in the lambda expression that
defines the body of the handler. for_each(a, a+5,
LL::for_each(_1, _1 + 10, _1 += 6));
The second handler catches exceptions from the
standard library, writes an informative message to Notice the reuse of _1. In the first two arguments
the cout stream and constructs and throws an ex- of the inner for_each it refers to the elements over
ception of another type, bar_ex. An object of which the outer for_each iterates. In the third ar-
type std::exception carries a string explaining gument, the same placeholder refers to the elements
the cause of the exception. This explanation can in the inner for_each.
be queried with the zero-argument what member
function; bind(&std::exception::what, _E) is the
lambda expression for creating the lambda functor for 4 About implementation
calling what. Note the use of _E as the argument. It
is a special placeholder, which refers to the caught The LL implementation is based on expression tem-
exception object within the handler body. _E is not plates [13]. The basic idea behind expression tem-
a full-fledged placeholder, but rather a special case of plates is to overload operators to create expression
_3. As a consequence, _E cannot be used outside of objects to represent the expression and its arguments
an exception handler lambda expression, and _3 can- instead of evaluating the operator instantly. As a re-
not be used inside of an exception handler lambda sult, the type of an expression object can describe

10
a parse tree of the underlying expression. This ex- other words passing and storing bound arguments as
pression object can be manipulated in various ways non-const references, requires quite some trickery. An
(also at compile time) prior to actually evaluating it, interested reader should read the technical report [4]
for example to yield better performance by prevent- and take a look at the code. The library can be down-
ing the creation of unnecessary temporary objects. loaded at lambda.cs.utu.fi.
In the LL case, this manipulation means passing the
expression object as a parameter to a function, and
substituting the actual arguments for the placeholder 5 About performance and use
objects.
As we said in section 2.2, LL placeholders do not In theory, all overhead of using STL algorithms and
carry information about the types of the open ar- lambda functors compared to hand written loops can
guments. This leaves another task, return type de- be optimized away, just as the overhead from stan-
duction, to be performed by the expression template dard STL function objects and binders. Depending
machinery. This is accomplished with a set of traits on the compiler, this can also be true in practice. Our
templates. As input, these templates take the under- tests suggest that the LL does not introduce a loss
lying operator, i.e. the target function, of the lambda of performance compared to STL function objects.
functor together with the types of the arguments this Hence, with a reasonable optimizing compiler, using
operator will be called with. The return type deduc- simple lambda expressions you are no worse off than
tion templates are specialized with respect to the type using classic STL. Further, with a great optimizing
of the target function (different templates for arith- compiler there is no penalty at all.
metic operators, assignment operators, etc.). Each Another issue is the impact the LL has on compile
such specialization provides a mapping from the ar- times. Expression templates usually involve recursive
gument types to the return type. If the argument template instantiations and can slow down compila-
types are lambda functor instantiations as well, the tion considerably. The LL is no exception, especially
compiler has to resort to the return type deduction deeply nested lambda expressions can be very slow to
system recursively to figure out the actual argument compile.
types. As another downside, compilation error messages
The LL attempts to cover a fairly complete set of that result from invalid lambda expressions can be
expression types. This means that there are many very hard to comprehend. Even a very simple lambda
sources of variability: the arity of the lambda func- functor has a type that spans several lines in an error
tors, the arity of the target functions, the call syn- message.
tax of the target functions (member functions, non-
member functions, operators), boundedness of argu-
ments etc. To be able to cope with the variability 6 Conclusion
with a linear, rather than a combinatorial number of
template definitions, the LL consists of several layers. With the Lambda Library we hope to provide a valu-
Each layer implements a certain task orthogonal to able set of tools for working with STL algorithms.
the tasks of the other layers. Within each layer, it is The LL removes several restrictions and simplifies the
enough to write template specializations with respect use of the STL in many ways. The users of the LL
to a single varying factor. have a natural way to write simple functors cleanly.
The LL implementation is far from trivial. Espe- Teachers of STL algorithms can have students writ-
cially, the return type deduction templates, as well ing clear code quickly: it is easier to explain how to
as the argument substitution code are rather com- use the LL than the current alternative of ptr_fun,
plicated. (The proposed language extension typeof mem_fun, bind1st, etc. and it extends better to the
would simplify the return type deduction code.) Fur- more complex problems (cf. bind3rdAnd4th). Fur-
ther, allowing side effects via bound arguments, in ther, the LL introduces a set of entirely new possibil-

11
ities for STL algorithm reuse: The LL makes it pos- [7] V. Simonis. Adapters and binders - overcoming
sible to loop through multiple nested containers. Ex- problems in the design and implementation of
ceptions can be thrown, caught and handled within the C++-STL. ACM SIGPLAN Notices, Jan-
the functor, and the looping in the STL algorithm can uary 2000.
be continued. All the above features are built with
standard C++ templates and do not change the de- [8] G. Powell and P. Higley. Expression templates as
sign model of the language or require an additional a replacement for simple functors. C++ Report,
preprocessing step. 12(5), May 2000.
We are aware of the downsides of the library: com- [9] The FACT! library home page, 2000. http://
plexity, increased compile times and difficult error www.fz-juelich.de/zam/FACT.
messages. These are problems with classic STL as
well, albeit harder in the LL. Despite the problems, [10] B. McNamara and Y. Smaragdakis. Func-
STL became extremely popular. The extensions that tional Programming in C++. In Proceedings
the LL adds to STL have potential for being adopted of The 2000 International Conference on Func-
in wide use as well. There is always room for improve- tional Programming (ICFP). ACM, 2000. http:
ment, but we believe that in terms of generality, ease //www.cc.gatech.edu/~yannis/fc++.
of use and intuitiveness of writing function objects
for STL algorithms, the tools in LL are getting close [11] J. Striegnitz and S. A. Smith. An expression
to what can be achieved without changes to the core template aware lambda function. In First Work-
language. shop on C++ Template Programming, Erfurt,
Germany, October 2000.
[12] C++ boost community. http://www.boost.
References org, 2000.
[1] Agents, iterators and introspection, 2000. http: [13] T. L. Veldhuizen. Expression templates. C++
//www.eiffel.com (information, papers). Report, 7(5):26–31, June 1995.
[2] A. A. Stepanov and M. Lee. The standard [14] J. Järvi and G. Powell. Side effects and par-
template library. Technical Report HPL-94- tial function application in C++. In Proceed-
34(R.1), Hewlett-Packard Laboratories, April ings of the Multiparadigm Programming with OO
1994. http://www.hpl.hp.com/techreports. Languages Workshop (MPOOL’01) at ECOOP
[3] International Standard, Programming Lan- 2001, John von Neumann Institute of Comput-
guages – C++, ISO/IEC:14882, 1998. ing series, 2001.

[4] J. Järvi and G. Powell. The Lambda Library


: Lambda abstraction in C++. Technical Re-
port 378, Turku Centre for Computer Science,
November 2000.

[5] The SGI Standard Template Library. http://


www.sgi.com/tech/stl, 2000.

[6] J. Järvi. C++ function object binders made


easy. In Proceedings of the Generative and
Component-Based Software Engineering’99, vol-
ume 1799 of Lecture Notes in Computer Science,
August 2000.

12

You might also like