Home > OS >  C confusing closure captures [v] vs [v = v]
C confusing closure captures [v] vs [v = v]

Time:07-07

In the following code, it seems that the compiler sometimes prefer to call the templated constructor and fails to compile when a copy constructor should be just fine. The behavior seems to change depending on whether the value is captured as [v] or [v = v], I thought those should be exactly the same thing. What am I missing?

I'm using gcc 11.2.0 and compiling it with "g file.cpp -std=C 17"

#include <functional>
#include <iostream>
#include <string>

using namespace std;

template <class T>
struct record {
  explicit record(const T& v) : value(v) {}

  record(const record& other) = default;
  record(record&& other) = default;

  template <class U>
  record(U&& v) : value(forward<U>(v)) {} // Removing out this constructor fixes print1

  string value;
};

void call(const std::function<void()>& func) { func(); }

void print1(const record<string>& v) {
  call([v]() { cout << v.value << endl; }); // This does not compile, why?
}

void print2(const record<string>& v) {
  call([v = v]() { cout << v.value << endl; }); // this compiles fine
}

int main() {
  record<string> v("yo");
  print1(v);
  return 0;
}

CodePudding user response:

For [v], the type of the lambda internal member variable v is const record, so when you

void call(const std::function<void()>&);

void print1(const record<string>& v) {
  call([v] { });
}

Since [v] {} is a prvalue, when it initializes const std::function&, v will be copied with const record&&, and the template constructor will be chosen because it is not constrained.

In order to invoke v's copy constructor, you can do

void call(const std::function<void()>&);

void print1(const record<string>& v) {
  auto l = [v] { };
  call(l);
}

For [v=v], the type of the member variable v inside the lambda is record, so when the prvalue lambda initializes std::function, it will directly invoke the record's move constructor since record&& better matches.

CodePudding user response:

I don't disagree with 康桓瑋's answer, but I found it a little hard to follow, so let me explain it with a different example. Consider the following program:

#include <functional>
#include <iostream>
#include <typeinfo>
#include <type_traits>

struct tracer {
  tracer() { std::cout << "default constructed\n"; }
  tracer(const tracer &) { std::cout << "copy constructed\n"; }
  tracer(tracer &&) { std::cout << "move constructed\n"; }
  template<typename T> tracer(T &&t) {
    if constexpr (std::is_same_v<T, const tracer>)
      std::cout << "template constructed (const rvalue)\n";
    else if constexpr (std::is_same_v<T, tracer&>)
      std::cout << "template constructed (lvalue)\n";
    else
      std::cout << "template constructed (other ["
                << typeid(T).name() << "])\n";
  }
};

int
main()
{
  using fn_t = std::function<void()>;

  const tracer t;
  std::cout << "==== value capture ====\n";
  fn_t([t]() {});
  std::cout << "==== init capture ====\n";
  fn_t([t = t]() {});
}

When run, this program outputs the following:

default constructed
==== value capture ====
copy constructed
template constructed (const rvalue)
==== init capture ====
copy constructed
move constructed

So what's going on here? First, note in both cases, the compiler must materialize a temporary lambda object to pass into the constructor for fn_t. Then, the constructor of fn_t must make a copy of the lambda object to hold on to it. (Since in general the std::function may outlive the lambda that was passed in to its constructor, it cannot retain the lambda by reference only.)

In the first case (value capture), the type of the captured t is exactly the type of t, namely const tracer. So you can think of the unnamed type of the lambda object as some kind of compiler-defined struct that contains a field of type const tracer. Let's give this structure a fake name of LAMBDA_T. So the argument to the constructor to fn_t is of type LAMBDA_T&&, and an expression that accesses the field inside is consequently of type const tracer&&, which matches the template constructor's forwarding reference better than the actual copy constructor. (In overload resolution rvalues prefer binding to rvalue references over binding to const lvalue references when both are available.)

In the second case (init capture), the type of the captured t = t is equivalent to the type of tnew in a declaration like auto tnew = told, namely tracer. So now the field in our internal LAMBDA_T structure is going to be of type tracer rather than const tracer, and when an argument of type LAMBDA_T&& to fn_t's constructor must be move-copied, the compiler will chose tracer's normal move constructor for moving that field.

  • Related