Modern C++ features – lambdas

Contents

Having covered the basics of `auto` and rvalue references, there is a third big new C++ feature definitely worth knowing about: creating function objects on the fly with lambda expressions.

The need for single use functions

Have you ever had to sort a vector in a way that was not a natural ascending order provided by `std::less` (that is operator< for most uses)? You probably had, because that’s a very common thing to do.

In C++03 it also was a tedious thing to do. You basically had to write a predicate function or function object that took two elements of your vector and told you if the first element should be sorted into the vector before the second one or not, and then call `std::sort` with the container’s begin and end iterators and that predicate as arguments.

bool hasMoreGold(Hero const& h1, Hero const& h2) {
  return h1.inventory().gold() < h2.inventory().gold();
}

vector<Hero> heroes;
//...
std::sort(heroes.begin(), heroes.end(), &hasMoreGold);

The tedious part was that you had to define the predicate outside the function you were currently writing, even if it was just a simple short one-liner. It also could make the code more cumbersome to read, if the author did not come up with a meaningful name for the predicate.

But this was only the easy part. What about removing elements based on some condition that depends on a previously calculated value? Now the predicate has to be a function object, and you have to initialize it with the parameters it needs:

struct HasLessGoldThan {
  unsigned threshold;
  bool operator()(Hero const& hero) {
    return hero.inventory().gold() < threshold;
  }
  HasLessGoldThan(unsigned ui) : threshold(ui) {}
};

vector<Hero> heroes;
//...
vector<Hero>::iterator newEnd = std::remove_if(heroes.begin(), heroes.end(), HasLessGoldThan(5u));
heroes.erase(newEnd, heroes.end());

Yuck! It gets a bit better in C++11 with `auto` for `newEnd` and uniform initialization which lets you skip the constructor definition, but you still have to write that clumsy helper class, put it in some awkward place and make sure it has internal linkage. And all just for a single algorithm call.

C++11: Lambdas to the rescue

A lambda is an expression that generates a function object on the fly. The function object itself is called a closure. It is best explained by means of showing how the first sorting example will look with a lambda expression:

vector<Hero> heroes;
//...
std::sort(heroes.begin(), heroes.end(), 
  [](Hero const& h1, Hero const& h2) -> bool {
    return h1.inventory().gold() < h2.inventory().gold();
  }
);

Here we see, that the predicate function is gone, but its body is at the place where it was passed into the function, preceded by what I’d call the lambda signature:

Two square brackets `[ ]` – they are the first part of every lambda expression. So, if you see a pair of those without a preceding identifier, then you have a lambda. (With the preceding identifier you have an array access or declaration.) They contain an optional capture list, but I’ll come to those later in detail when I handle the second example. For now, we’ll leave them empty.

A function parameter list. It looks like any other function parameter list, with two exceptions: Default parameters are not allowed until C++14, and since C++14 you can use `auto` for the parameter types. It essentially converts the function call operator of the closure into a template, without giving the template parameters names. I will cover this in more detail later.

A trailing return type. This one works the same way as trailing return types for normal functions. Only for a lambda expression this is the only option to explicitly state the return type, so no old school return type syntax is allowed.

As in normal functions, you can omit the trailing return type since C++14, the compiler then will deduce it for you in accordance to the normal rules. Return type deduction is also allowed in C++11, but only in lambda expressions, and only in a small set of cases: If the lambda consists of a single return expression,the return type is deduced to be the type of that expression. In all other cases the return type is void.

If the return type is omitted, i.e. if we let the compiler deduce it, then we are also allowed to omit the function parameter list. In that case, the function call operator of the closure does take no parameters, i.e. it is equivalent to an empty parameter list.

That way the shortest possible and pretty useless lambda expression is `[]{}`: Empty square brackets, omitted parameter list and return type, empty body. It generates a closure whose function call operator takes no arguments, does nothing and returns `void`.

The closure object

A lambda expression does several things at once: It defines the function object class with its function call operator on the fly and creates a single object, the closure object.

Closure objects are a curious thing. They have a specific type, but you can not name it. For all purposes except for the compiler’s inner workings, the type has no name. If you have two identical lambda expressions, they will produce closures of two different types regardless.

You can capture and have an alias or typedef for a closure type by using `decltype`, but since you most often will use lambdas for single use throwaway purposes, this should be a relatively rare use case.

The most obvious member of the closure type is the function call operator. It has the provided signature, except that it is const-qualified, i.e. the closure object can not be changed.

Another important member is the implicit conversion to a function pointer. The type corresponds to that of a function with the same parameter list and return type that provided in the lambda signature. So if you encounter an old fashioned callback in the form of a function pointer instead of “anything callable”, you can pass it a lambda, as long as it has an empty capture list and the correct parameter list.

There are other members of the closure type: It has a deleted default constructor, so you can’t use the aforementioned typedef to create an independent second object of the same type. Assignment operators are also deleted, so you can’t change a closure object by assigning it an old version of itself.

Closure types have defaulted copy and move constructors as well as a defaulted destructor. As long as the lambda expression has no capture list, all those special members do nothing.

Next up: closures with state

By now, there are some loose ends in this post: I did not solve the `remove_if` example, where I had to use a functor with a member variable, with lambdas. I mentioned the possibility of a capture list but did not further explain it. I have mentioned the constness of the function call operator and the defaulted copy and move constructors and destructor.

All this wraps up with a simple fact: using the capture-list we are able to create closure objects that have internal state. However, this complicates matters slightly, so I will have to defer that topic to my next post.

Previous Post
Next Post

Leave a Reply

Your email address will not be published. Required fields are marked *