Some notes on C++11 lambda functions

Lambda functions are a new capability introduced in C++ that offers a terse compact syntax for defining functions at the point of their use. Bjarne Stroustrup says that C++11, which is the latest ratified revision of the C++ standard, "feels like a new language". I think lambda functions are a big part of what makes the language feel so very different from C++03. Lambda functions basically allow you to do things like this:

vector<int> nums { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto evens = count_if(begin(nums), end(nums), [](int num) {
    return (num % 2) == 0;
});

The third parameter passed to the standard count_if function is a predicate that is expected to return true if the value passed to it satisfies the condition and false otherwise. In the snippet above we simply count the number of instances of even numbers in the collection. Search for "C++ lambdas" on your favorite search engine and you should get plenty of material out there talking about this feature. What follows in this post are some notes on certain aspects of C++ lambdas that I happened to notice as I was learning about them listed in no particular order.

  1. You can pass a lambda object around as you would pretty much anything else. Here's a made up example showing how you can pass a lambda as an argument to another function.

    #include <iostream>
    
    using namespace std;
    
    template <typename T>
    void call(T);
    
    int main() {
      auto fn = []() { cout<<"Lambda"<<endl; };
      call(fn);
      return 0;
    }
    
    template <typename T>
    void call(T fn) {
      fn();
    }
    
  2. You can return lambdas from functions like any another object which makes for some interesting possibilities such as the following:

    #include <iostream>
    #include <functional>
    
    using namespace std;
    
    template<typename T>
    function<T()> makeAccumulator(T& val, T by) {
        return [=,&val]() {
            return (val += by);
        };
    }
    
    int main() {
        int val = 10;
        auto add5 = makeAccumulator(val, 5);
        cout<<add5()<<endl;
        cout<<add5()<<endl;
        cout<<add5()<<endl;
        cout<<endl;
    
        val = 100;
        auto add10 = makeAccumulator(val, 10);
        cout<<add10()<<endl;
        cout<<add10()<<endl;
        cout<<add10()<<endl;
    
        return 0;
    }
    

    Which produces the following output:

    15
    20
    25
    
    110
    120
    130
    

    The key thing to remember here is that it is your responsibility to make sure that the values you capture in a lambda remain in memory for the lifetime of the lambda itself. The compiler will not for instance, prevent you from capturing local variables by reference in a lambda and if you continue to access a variable that is no longer available, well, then the behavior is undefined.

  3. Simply defining a lambda causes all variables captured by value by the lambda to be copy constructed. You don't really have to have any code that invokes the lambda in order for the variables to be copied. This is consistent with the idea that creating a lambda function essentially creates a function object which has as instance members the variables that have been captured in the lambda. Here's an example:

    #include <iostream>
    
    using namespace std;
    
    class Foo {
    public:
      Foo() {
        cout<<"Foo::Foo()"<<endl;
      }
    
      Foo(const Foo& f) {
        cout<<"Foo::Foo(const Foo&)"<<endl;
      }
    
      ~Foo() {
        cout<<"Foo~Foo()"<<endl;
      }
    };
    
    int main() {
      Foo f;
      auto fn = [f]() { cout<<"lambda"<<endl; };
      cout<<"Quitting."<<endl;
      return 0;
    }
    

    Here's the output this produces:

    Foo::Foo()
    Foo::Foo(const Foo&)
    Quitting.
    Foo~Foo()
    Foo~Foo()
    

    As you can tell, the copy constructor gets invoked even though the lambda itself never gets invoked.

  4. As an extension of the previous point, if you capture objects by value in a lambda and then proceed to pass that lambda around to other functions by value, then the variables in the closure will also get copy constructed.

  5. If you reference capture a const local variable, it becomes a const reference in the lambda.

    #include <iostream>
    
    using namespace std;
    
    int main() {
        const int val = 10;
        auto f1 = [&val]() {
            val = 20;  // won't compile
            cout<<"val = "<<val<<endl;
        };
        f1();
    
        return 0;
    }
    
  6. In general, when you wish to declare a variable that can hold a reference to a lambda in contexts where auto is not permissible (for e.g. function return types or arguments) use std::function. An example:

    #include <iostream>
    #include <functional>
    
    using namespace std;
    
    function<bool(int)> makeLEPredicate(int max) {
        return [max](int val) -> bool {
            return val <= max;
        };
    }
    
    int main() {
        auto le10 = makeLEPredicate(10);
        cout<<le10(8)<<endl;
    
        return 0;
    }
    

    You could alternatively use function templates to achieve the same thing if the semantics of using templates makes sense to your use case.

That's all for now. This list might get expanded as I explore lambdas further. As you might have noticed, the ability to use lambdas really does make a significant difference to productivity without sacrificing performance.

comments powered by Disqus