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
4void 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_ifalgorithm by passing a regular function.- Works, because
predicateis equivalent to&predicate1
2
3
4
5bool predicate(const Foo&);
void foo()
{
std::find_if(c.begin(), c.end(), predicate);
}
- Works, because
Pointers to functions can be invoked with
( )1
2
3f_ptr();
// is equivalent to
(*f_ptr)();Following example fails to compile, as
predicateis now ambiguous1
2
3
4
5
6bool 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
predicateis an instance of a function object1
2
3
4
5const auto predicate = [](const auto&){ /* ... */ };
void foo()
{
std::find_if(c.begin(), c.end(), predicate);
}
- Need to explicitly choose overload - example:
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_functionbinary_function
- Binders
binder1stbinder2ndbind1stbind2nd
- ** … **
...
- Base
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_fnstd::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)
- Overloaded member functions (need
- 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.
- Affects
- 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_fnreturns a call wrapper of unspecified type that can be invoked
1 | struct foo |
1 | auto foo_print = std::mem_fn(&foo::print); |
- Like
std::bindand function pointers, it fails on overloads/templates
1 | struct foo |
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 | auto foo_print = [](auto&& this_foo) |
std::bind Versus Lambda Expressions
- Given a function object
fand some arguments,std::bindreturns a call wrapper of unspecified type equivalent to invokingfwith some of its arguments bound
1 | int add(int a, int b) { return a + b; } |
1 | // 1st arg is fixed to 10, 2nd arg is a placeholder that user can pass the arg to |
- Can work with overloads/templates
1 | int add(int a, int b) { return a + b; } |
1 | auto add10 = [](auto x) |
Summary
- Learned that closures produced by lambda expressions are
first-classcitizens - 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_fnandstd::bindand should be used instead