Jump to content

  • Log In with Google      Sign In   
  • Create Account





The New C++ - lambdas

Posted by Washu, 06 March 2012 · 2,704 views

C++ C++11 lambdas
Ah lambdas. If you’ve used any functional languages, python, ruby, or C# (or many other languages), you are probably familiar with the concept of lambdas. However, if you’ve been doing C++ for a while and haven’t used boost’s lambda library then let me be the first to introduce you to the concept… A lambda is, essentially, an unnamed (or anonymous) function. You can use lambdas as a simplification for many common scenarios that crop up frequently throughout code. Examples include a sort predicate, a complex search predicate, or an event system.

Lambda Basics
Probably the most common type of lambda you’ll encounter is one that simply takes a set of parameters and returns a result based on them. One of the simplest examples of this is the factorial function. The factorial function returns the product of all numbers from 1 to N, and is typically represented as N! (reads as “N factorial”. It grows extremely quickly, and will typically outpace the size of an integer in a few steps. If we take the code from our previous entries and use that as a starting point, we can use the v1 vector of integers and compute the factorial of 10:
auto factorial = [](int lhs, int rhs) { return lhs * rhs; };
    auto result = std::accumulate(v1.begin(), v1.end(), 1, factorial);
    std::cout<<result<<std::endl;
The lambda portion of the previous statements is clearly the definition of the factorial variable. The lambda syntax in C++ leaves much to be desired, but I shall endeavor to explain the syntax…

We can break it up into four basic parts, the capture statement, the parameters, the return type, and the body of the lambda:
[ capture-statement ] ( parameters ) /* optional if empty */ -> return-type /*optional*/ { body }
Capture statements and return-type need some explaining, the parameters and body though are pretty self-explanatory. The only thing I will state about the parameters argument is: if you have no arguments then the parenthesis are optional. I tend to insert them anyways, just for clarity.

The Capture Statement
The capture statement is used to capture variables that are not local to the body of the lambda. You can capture variables either by value or by reference, and you can also capture all variables available in a scope by value or by reference. Again, the syntax is a bit odd:
    auto push_back_c_v1_byref = [&v1](int i) { v1.push_back(i); };
    auto push_back_c_all_byref = [&](int i) { v1.push_back(i); };
    auto print_c_v1_byval = [v1]() { for each(auto x in v1) { std::cout<<x<<" "; }  };
    auto print_c_all_byval = [=]() { for each(auto x in v1) { std::cout<<x<<" "; }  };
    auto print_c_all_byval_v2_byref = [=,&v2]() { for each(auto x in v1) { std::cout<<x<<" "; } v2.push_back(9); };

    push_back_c_v1_byref(11);
    push_back_c_all_byref(11);
    print_c_v1_byval();
    print_c_all_byval();
The first two capture statements are reference captures. The first one, push_back_c_v1_byref, only captures the v1 vector by reference and nothing else is captured. Since we’re capturing v1 by reference we can manipulate v1 in various ways, such as (in this case) inserting an item into the vector. The second capture statement is known as a “default capture by reference” and captures everything in scope by reference. Thus we’re able to manipulate v1 without having to explicitly capture it. Obviously there is an efficiency concern here, however compilers should be able to optimize out any references to objects NOT used by the lambda.

The second two captures show by-value capturing. With the first one, print_c_v1_byval, taking a copy of v1. This does result in a copy constructor invocation. As such, for something like this print method, it’s not necessarily terribly efficient. Although, for value types or for types you want to ensure the lambda doesn’t modify, taking it by-value can be an advantage. The second capture uses the “default capture by value”, and much like the “default capture by reference”, the compiler will likely optimize out anything you don’t explicitly use. The for-each syntax we’ll get into later, but it’s a Visual Studio 2010 (and VS11) extension to the language. The C++11 version is for(T v : container).

The last capture statement is an interesting one. We declare the default capture to be by-value, but then we explicitly state that we wish to capture v2 by reference. Note that this is also possible to be done vice-versa. You can capture everything by reference except those items you explicitly designate to be by-value.

The Return Type
The return type of a lambda is the one optional component of the lambda declaration (as you may have noticed in the examples above, I’ve omitted the return type for all of them). The return type is only mandatory when the type cannot be inferred from the body of the lambda, or when the return type does not match what you are attempting to return. Here’s a simple example:
auto print_hello_cstr = []() { return "hello"; };
    auto print_hello_stdstr = []() -> std::string { return "hello"; };

    std::cout<<print_hello_cstr()<<std::endl;
    std::cout<<print_hello_stdstr()<<std::endl;
Now, clearly the first one returns a char const*, as that’s the C++ string literal type. The second one, however, returns a string type, invoking the appropriate constructor of the string type. There are other cases though where you might find that you have to explicitly specify the result type,
auto return_something = [](int a) { if(a > 40) return 4.5f; return 3; };

This lambda poses a bit of a problem, there are actually two problems here. The first is that the return type is not clear (it could be float or int), and the conditional does nothing to clarify it. Moreover, simply changing the literal integer to a literal float doesn’t solve all of the issues, and so the standard requires you to specify the expected return type if the lambda is more than just a simple return statement. We can fix this in one of two ways, in the first way we reduce the statement to a simple return:
auto return_something = [](int a) { return (a > 40) ? 4.5f : 3; };
While in the second method we simply indicate the expected return type:
auto return_something = [](int a) -> float { if(a > 40) return 4.5f; return 3; };

Applying Our Knowledge
We’ve gone through all of this effort to reach this point, going back to our original piece of code, which we’ve updated with auto and decltype, we have end up with the resulting piece of code:
#include <iostream>
#include <vector>
#include <functional>

template<class Sequence1, class Sequence2, class MatchBinaryFunctor>
auto find_first_pair(Sequence1 const& seq1, Sequence2 const& seq2, MatchBinaryFunctor match) -> decltype(std::make_pair(seq1.end(), seq2.end()))
{
    for(auto itor1 = seq1.begin(); itor1 != seq1.end(); ++itor1) {
        for(auto itor2 = seq2.begin(); itor2 != seq2.end(); ++itor2) {
            if(match(*itor1, *itor2)) {
                return std::make_pair(itor1, itor2);
            }
        }
    }

    return std::make_pair(seq1.end(), seq2.end());
}

bool is_equal_and_odd(int lhs, int rhs) {
    return lhs == rhs && lhs % 2 != 0;
}

int main() {
    int v1ints[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int v2ints[] = {13, 4, 5, 7};

    std::vector<int> v1(v1ints, v1ints + 10);
    std::vector<int> v2(v2ints, v2ints + 4);

    auto p = find_first_pair(v1, v2, is_equal_and_odd);

    std::cout<<*p.first<<":"<<*p.second<<std::endl;
}
The only real outstanding issue, at the moment, is the function is_equal_and_odd. This function is exactly the kind of thing lambdas were designed to help eliminate. Thus if we apply our knowledge of lambdas we can come up with a quick replacement to eliminate that entire function, inlining it into the function that’s doing the work:
int main() {
    int v1ints[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int v2ints[] = {13, 4, 5, 7};

    std::vector<int> v1(v1ints, v1ints + 10);
    std::vector<int> v2(v2ints, v2ints + 4);

    auto p = find_first_pair(v1, v2, [](int lhs, int rhs) { return lhs == rhs && lhs % 2 != 0; });

    std::cout<<*p.first<<":"<<*p.second<<std::endl;
}
Which is, in my opinion, quite a bit cleaner and simpler than having a separate function definition hanging around.




Great post.

For me, Lambdas make coding in c++ exciting again. The only thing I can compare it to is the fun of using lambdas / list comprehensions in Python - the code just flows from your fingertips as fast as you can think. Being able to use std algortihms without explicitly defining a functor makes a huge difference - even if I decide later that I'm going to use the lambda's logic in a few different scopes and it's worth redefining it as an explicit functor, the convenience of being able to write the initial logic without breaking flow to go away and write boilerplate is a huge productivity win.

I found lambda syntax a bit odd at first too, but I really hope it becomes a mainstream part of the average c++ programmer's mental toolset. For all it's terseness, I think in time it will actually improve readability of c++ codebases. When trying to understand a bit of code, it's sometimes much easier to read some logic in place (in a lambda) than to have to go and read the header for a simple adapter or functor.

One of the coolest uses of lambdas (I think) is to use them in place of 'bind' to combine functors or to do a sort of polymorphism of parameterisation. I've no idea what to call this, but essentially most situations where you want to have a collection of arbitrary functions to call on arbitrary data you can use lambdas to bundle up the logic and data into an object whose operator() that takes the appropriate minimal number and type of parameters for the collection. Unfortunately, lambda types being unrelated means you either have to use std::function or a template parameter to talk about your 'minimal' abstract functor interface. Maybe we'll get polymorphic lambdas (i.e. of related types) at some point. Still, a great way to write little adapters without needing lots of boilerplate.
PARTNERS