bgp2000 bgp2000 - 1 year ago 46
C++ Question

Just when are lambda captures initialized?

In the following function, can I rely on the captured variable 'order' to be up to date, i.e.

  1. Will the function always work as expected?

  2. Is there a difference between capturing by value or reference.

  3. Is the function reentrant?

struct Entry
std::string name;
double earnings;

enum Column { Name, Earnings };
enum SortOrder { Ascending, Descending };

void sortByColumn(std::vector<Entry>& entries, Column column, SortOrder order)
std::function<bool(const Entry&, const Entry&)> comparators[] =
[&](const Entry& a, const Entry& b) { return order==Ascending ? < : >; },
[=](const Entry& a, const Entry& b) { return order==Ascending ?
a.earnings < b.earnings : a.earnings > b.earnings; }
std::sort(entries.begin(), entries.end(), comparators[column]);

You can find a full example here:

Answer Source

The thing that you must recognize is that a lambda is a closure type object:

The lambda expression is a prvalue expression an unnamed temporary object of unique unnamed non-union non-aggregate class type, known as closure type, which is declared (for the purposes of Argument-Dependent Lookup) in the smallest block scope, class scope, or namespace scope that contains the lambda expression[source]

You can think of the lambda's capture list as constructor arguments. In this case you passed order by value/reference into the 1st/2nd comparators respectively. Since order does not change over the lifetime of comparators, passing by reference or value will have identical results in your example.

But if this function were called with order set to Ascending and you added this to the bottom of sortByColumn:

order = Descending;
std::sort(entries.begin(), entries.end(), comparators[0]);

You would have sorted entries by name in descending order. Meaning the change to order effected the lambda.

If you'd done this however, and sortByColumn was again passed order as Ascending:

order = Descending;
std::sort(entries.begin(), entries.end(), comparators[1]);

You would have sorted entries by earnings in ascending order. Meaning the change to order did not effect the lambda.

When deciding whether to capture by reference the same considerations should be applied that you use in deciding whether or not to use a reference or copy as an object member:

  1. You must use a reference if the object cannot be copy constructed, and you might choose to use a reference if copy construction is expensive
  2. You must make a copy if the lifetime of the lambda is going to exceed the lifetime of what it's capturing, and you might choose to use make a copy if you are providing the lambda to be used externally to your code