Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

capturing everything vs. capturing only some in a lambda

Tags:

c++

lambda

c++14

Every now and then I come up with code like this:

// lazy lambda:
[&] {
// use references to a, b, c
}

// pedantic lambda
[&a, &b, &c] {
// use references to a, b, c
}

I'd like to know which of the lambdas is better in terms of performance and perhaps executable size according to the C++14 standard (or later) and your practical experience with compilers.

like image 845
user1095108 Avatar asked Nov 30 '15 13:11

user1095108


2 Answers

There is no difference in this example. The compiler will only capture variables that are explicitly referenced in the lambda, making the lazy capture equivalent to the explicit one.

A lazy capture is slightly more convenient. Changing the body of the lambda to use additional captured variables, or removing all references to a captured variable from an existing lambda, would normally also require updating an explicit capture list. With a lazy capture the compiler will do everything for you.

like image 51
Sam Varshavchik Avatar answered Sep 19 '22 17:09

Sam Varshavchik


Excluding the trivial case where you explicitly capture something that isn't mentioned in the lambda at all, there are two corner cases where there might be a difference.

First, implicit captures generally do not capture entities that aren't odr-used (but see the next item for the exception to this). This includes, among other things, things mentioned in unevaluated operands such as those of decltype and sizeof, as well as certain const and constexpr local variables when used in certain contexts (consult [basic.def.odr] for the full set of rules):

void f(int){}
void f2(const int &) {}
void t() {
    const int x = 8;
    constexpr double y = 8;
    const double z = 8; 
    auto g = []{ f(x); }; // OK
    auto g1 = [=]{ f(x); }; // OK, does not capture x

    // usually won't fire, though not guaranteed
    static_assert(sizeof(g) == sizeof(g1), "!!"); 

    auto g2 = []{ f(y); }; // OK
    auto g3 = []{ f(z); }; // Error, must capture z

    auto g4 = []{ f2(x); };  // Error, must capture x since it's odr-used
    auto g5 = [=]{ f2(x); }; // OK, captures x
    auto g6 = []{ f2(+x); }; // OK, doesn't odr-use x
    auto g7 = []{ f2(y); };  // OK
}

If you write the capture list manually, it's possible that you'll capture more than you technically need to, because the rules governing what is or isn't odr-used are pretty complex.

Second, implicit captures in generic lambdas will capture things used in a dependent expression, even when they aren't necessarily odr-used, for sanity. Borrowing an example from the standard:

void f(int, const int (&)[2] = {})    { }   // #1
void f(const int&, const int (&)[1])  { }   // #2
void test() {
  const int x = 17;
  auto g2 = [=](auto a) {
    int selector[sizeof(a) == 1 ? 1 : 2]{};
    f(x, selector);  // OK: is a dependent expression, so captures x
                     // Will actually odr-use x only if sizeof(a) == 1
  };
}

However, you are not actually required to capture something that may-or-may-not-be-odr-used when writing a generic lambda; you are only required to capture it if you instantiate a specialization of the function call operator that odr-uses the thing. And so if you never call the resulting generic lambda in a way that would odr-use the thing at issue, then the implicit capture may capture more than the minimum you need. For example, this is allowed:

void test() {
  const int x = 17;
  auto g3 = [](auto a) { // no capture
    int selector[sizeof(a) == 1 ? 1 : 2]{};
    f(x, selector);  // OK for now
  };
}

as long as you don't call g3 with anything whose size is 1: g3(0) is OK on typical systems; g3('\0') isn't.

like image 45
T.C. Avatar answered Sep 18 '22 17:09

T.C.