16

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;
}
becca
  • 183
  • 5
  • 2
    What is the error? – bolov Jul 06 '22 at 02:58
  • 7
    Stop doing `using namespace std;` in namespace/global scope! – Oasin Jul 06 '22 at 02:59
  • @bolov the error is quite long, but it basically tries to use the templated constructor and fails because value cannot be constructed from a record, which is werid, I feel like the compiler picked up the wrong constructor. I get the same thing with clang, so I must be missing something. – becca Jul 06 '22 at 03:03
  • 1
    Why do you have both a const and non const copy constructor defaulted? – doug Jul 06 '22 at 03:06
  • @doug I removed it but I still get the same error – becca Jul 06 '22 at 03:08
  • 2
    Part of the reason is this: https://stackoverflow.com/questions/57909923/why-is-template-constructor-preferred-to-copy-constructor -- this explains the compilation error, but does not explain why `[v = v]` fixes it. Someone else will need to figure that part out. – Sam Varshavchik Jul 06 '22 at 03:10
  • 1
    I'm not sure why, but the simple capture lambda is treating (a copy of?) `v` as a const rvalue, and so the template with `U = const record` (meaning the type of `v` is `const record&&`) is being selected since it's the best choice in that scenario. I don't know why it's treating `v` as a const rvalue though. – Miles Budnek Jul 06 '22 at 03:19
  • Thanks @SamVarshavchik, the SFINAE solution solved my problem, I'm still confused about the diffence between [v] and [v = v] though. – becca Jul 06 '22 at 03:20
  • 1
    If it is `[v]`, then the type of the lambda member variable is `const record`; if it is `[v=v]`, then the type of the lambda member variable is `record`. – 康桓瑋 Jul 06 '22 at 03:39
  • The const capture by lambdas is a key ingredient : some of my explorations here https://godbolt.org/z/nG4snGqfc – Pepijn Kramer Jul 06 '22 at 03:57
  • In fact, this question is a bit more complicated than those duplicates, which involves more than one thing, so I chose to open it. – 康桓瑋 Jul 06 '22 at 04:13
  • Three Possible dupes: [Why isn't this templated move constructor being called?](https://stackoverflow.com/questions/72518781/why-isnt-this-templated-move-constructor-being-called), [Why is template constructor preferred to copy constructor?](https://stackoverflow.com/questions/57909923/why-is-template-constructor-preferred-to-copy-constructor) and [Implicit type in lambda capture](https://stackoverflow.com/questions/65471002/implicit-type-in-lambda-capture) – Jason Jul 06 '22 at 04:33
  • See also https://stackoverflow.com/questions/68734721/why-do-fields-in-non-mutable-lambdas-use-const-when-capturing-const-values-or – Jeff Garrett Jul 06 '22 at 12:39
  • There are two pitfalls here that I fell into as well: 1. the templated constructor might take the class itself as its type 2. template record(U&&) might not be a move constructor but a forwarwarding (having a rvalue-reference instead of a universal reference) so that record(const U&&) is added to the list – Matthias Bäßler Jul 13 '22 at 06:43

2 Answers2

8

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 = t, 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 choose tracer's normal move constructor for moving that field.

user3188445
  • 4,062
  • 16
  • 26
6

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.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • 1
    Why is `v` copied? You seem to imply `v` is copied once for the prvalue, then again for `call`. But the whole prvalue of the lambda expression is materialized once and never copied, so why would `v` need to be copied more than once? – user3188445 Jul 06 '22 at 05:15
  • 1
    There is no materialization here, `std::function` is *always* copied. – 康桓瑋 Jul 06 '22 at 06:01