0%

Lambdas as First-Class Citizens in C++

This is my learning notes from the course - Mastering C++ Standard Library Features.

Lambdas as First-Class Citizens

  • What it means to be a “first-class citizen
  • How lambdas replace old Standard Library utilities
  • Templates and lambda expressions
  • Type-erasure for lambdas: std::function
  • Higher-order functions

Lambdas: Versatile Tools

  • Meaning of “first-class citizen
  • Standard Library utilities replaced by lambdas

Lambdas: First-Class Citizens

In programming language design, a first-class citizen (also type, object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as an argument, returned from a function, modified, and assigned to a variable.

– from Wikipedia - First-class citizen

  • In C++, regular functions are not first-class citizens.

    1
    2
    3
    4
    void foo()
    {
    void bar(){ } // ERROR: nested function
    }

    Instead, if you use a lambda, you are able to do it and assign it to a variable.

  • An Example of invoking the std::find_if algorithm by passing a regular function.

    • Works, because predicate is equivalent to &predicate
      1
      2
      3
      4
      5
      bool predicate(const Foo&);
      void foo()
      {
      std::find_if(c.begin(), c.end(), predicate);
      }
  • Pointers to functions can be invoked with ( )

    1
    2
    3
    f_ptr();
    // is equivalent to
    (*f_ptr)();
  • Following example fails to compile, as predicate is now ambiguous

    1
    2
    3
    4
    5
    6
    bool predicate(const Foo&);
    bool predicate(const Bar&);
    void foo()
    {
    std::find_if(c.begin, c.end(), predicate);
    }
    • Need to explicitly choose overload - example:
      1
      static_cast<bool(*)(const Foo&)>(predicate);
    • Use generic lambda
      • Compiles and works as intended
      • predicate is an instance of a function object
        1
        2
        3
        4
        5
        const auto predicate = [](const auto&){ /* ... */ };
        void foo()
        {
        std::find_if(c.begin(), c.end(), predicate);
        }
  • Recap

    • Closures produced by lambda expressions are first-class citizens
    • They can be used like any other object in C++
    • They can be used as first-class stateless functions or more flexible stateful function objects

Deprecated Standard Library Utilities Replaced by Lambdas

  • Many <functional> utilities were deprecated in C++11 and removed in C++17

    • Base
      • unary_function
      • binary_function
    • Binders
      • binder1st
      • binder2nd
      • bind1st
      • bind2nd
    • ** … **
      • ...
  • The behavior of these deprecated functions can be achieved by using lambda expressions

    1
    2
    3
    4
    5
    6
    7
    8
    // Deprecated:
    auto b0 = std::bind1st(f, 42);

    // Recommended:
    auto b0 = [](auto&& ... xs)
    {
    return f(42, std::forward<decltype(xs)>(xs) ... );
    };

C++11 Standard Library Utilities Inferior to Lambdas

  • The following utilities were either introduced in C++11:
    • std::mem_fn
    • std::bind
  • For various reasons, they are inferior to lambdas and should be avoided

What’s New, And Proper Usage

  • Stephan T. Lavavej, princple developer on the Visual C++ Libraries, gave an excellent talk at CppCon 2015: “<functional>: What’s New, And Proper Usage
  • In this talk, he explains the shortcomings of the aforementioned Standard Library functions
  • https://www.youtube.com/watch?v=zt7ThwVfap0
mem_fn() Isn’t Fun, Really
  • Good: Usually terse
  • Bad: Resistant to optimization
  • Ugly: Won’t compile in certain situations
    • Overloaded member functions (need static_cast)
    • Templated member functions (use static_cast)
      • Avoid explicit template arguments. Don’t help the compiler
    • Default arguments (no workaround)
  • Unnecessary with anything powered by invoke()

Recommendations

  • Avoid using mem_fn()
    • Algorithm inner loops often affect performance
    • As code evolves, mem_fn() is fragile
    • Auditing existing usage is low priority, though
  • Use lambdas, especially generic lambdas
    • They’re slightly more verbose
    • But they optimize away
    • And they always compile, like other member function calls
bind() Problems
  • Same performance/compiler issues as mem_fn()
    • With function pointers in addition to PMFs/PMDs
  • Misuse emits ultra-disgusting compiler errors
  • Syntax isn’t normal C++, especially nested bind()
  • No short-circuiting for logical_and/logical_or
  • Surprising behavior: bound args passed as lvalues
    • Affects unique_ptr, etc.
  • Surprising behavior: immediate vs. delayed calls
  • Placeholders and nested bind() can move twice

Recommendations

  • Avoid using bind()
  • Use lambdas, especially generic lambdas
  • bind(): good idea in 2005, bad idea in 2015
    • In C++, we usually prefer library solutions to Core
    • But the library is terrible at building up function objects
    • Lambdas were added to the Core Language for a reason
    • STL maintainers rarely recommend avoiding the STL
    • bind()‘s terseness just isn’t worth the price

std::mem_fn Versus Lambda Expressions

  • Given a member function pointer, std::mem_fn returns a call wrapper of unspecified type that can be invoked
1
2
3
4
5
struct foo
{
int _x = 0;
void print() { std::cout << _x << "\n"; }
};
1
2
3
auto foo_print = std::mem_fn(&foo::print);
foo_print(foo{}); // prints `0`
foo_print(foo{5}); // prints `5`
  • Like std::bind and function pointers, it fails on overloads/templates
1
2
3
4
5
struct foo
{
void print(int) { }
void print(long) { }
};
1
auto foo_print = std::mem_fn(&foo::print);

ERROR: no matching function for call to mem_fn(<unresolved overloaded function type>)

  • Using a lambda here is a zero-cost abstraction that works with overloads/templates
1
2
3
4
5
6
7
auto foo_print = [](auto&& this_foo)
{
this_foo.print();
};

foo_print(foo{}); // prints `0`
foo_print(foo{5}); // prints `5`

std::bind Versus Lambda Expressions

  • Given a function object f and some arguments, std::bind returns a call wrapper of unspecified type equivalent to invoking f with some of its arguments bound
1
int add(int a, int b) { return a + b; }
1
2
3
// 1st arg is fixed to 10, 2nd arg is a placeholder that user can pass the arg to
auto add10 = std::bind(add, 10, std::placeholders::_1);
std::cout << add10(10) << "\n"; // prints `20`
  • Can work with overloads/templates
1
2
int add(int a, int b)     { return a + b; }
int add(float a, float b) { return a + b; }
1
2
3
4
5
6
7
auto add10 = [](auto x)
{
return add(static_cast<decltype(x)>(10), x);
};

std::cout << add10(10) << "\n"; // prints `20`
std::cout << add10(10.5f) << "\n"; // prints `20.5`

Summary

  • Learned that closures produced by lambda expressions are first-class citizens
  • Understood that they can be assigned to variables, copied/moved, passed around, and so on
  • studied that many <functional> utilities have been deprecated in C++11
    • lambda expressions can be used instead
  • Understood that lambda expressions are superior to std::mem_fn and std::bind and should be used instead