Home > OS >  Why is copy constructor called so late in function call?
Why is copy constructor called so late in function call?

Time:12-09

In short, inside a function template, Foo, that is called by main, takes a universal reference and uses a field of supposedly local variable of Foo, it seems like the input parameter's (a reference to the local variable of main) fields are used when the field of "supposedly local variable of Foo" was referred.

My English skills are not too great to summarize my problem in a short paragraph first. Let me start with offering some backgrounds first.

I have a class that implements all five constructors intentionally wrongly:

class X {
public:
  X(const std::string& s) : s_(s) {
    std::cout << "X(const std::string&)" << std::endl;
  }
  X() {
    std::cout << "X()" << std::endl;
  }

  X(const X& x) : s_{"copy_con_val"} {
    std::cout << "X(const X&)" << std::endl;
  }

  X(X&& x) : s_{"move_con_val"} {
    std::cout << "X(X&&)" << std::endl;
  }

  X& operator=(const X& x)  {
    s_ = "copy_assign_val";
    std::cout << "X& operator=(const X&)" << std::endl;
    return *this;
  }

  X& operator=(X&& x) {
    s_ = "move_assign_val";
    std::cout << "X& operator=(X&&)" << std::endl;
    return *this;
  }

  std::string s_;
};

Please note that they are not using s_ from the source. I have a function template, Foo:

template <typename T>
T Foo(T&& t) {
  T out{std::forward<T>(t)};
  std::cout << "out = " << out.s_ << std::endl;
  return out;
}

Then, I have the caller, main:

int main(int argc, char* argv[], char* envp[])
{
  X x("name");
  // Expecting Foo to print "copy_con_value"
  auto y = Foo(x);
  std::cout << "all done: y = " << y.s_ << std::endl;
  return 0;
}

The output was different from what I expected, especially in the second line:

X(const std::string&)
out = name
X(const X&)
all done: y = copy_con_val

My understanding is, given the five constructors (let alone X(const string&)), the universal reference would become const X&. With regard to the return value optimization, the memory buffer for out is allocated in main. Especially, in this case, the memory buffer of out is the same as the memory buffer of y, the local variable of main that the return value is assigned to. The initialization would be effectively as follows:

// within Foo()'s scope
X main::y{static_cast<const X&>(main::x)};
X& out = main::y

Thus, when we use out within the scope of Foo, I believed the value of out.s_ belongs to the new object, not to the source (i.e. x). I believed copy_con_value should be printed.

However, it looks like out within the Foo function is the reference to x in main. It looks as if the copy construction is delayed as far as it could go. Only after returning to main, the code needs both x and y there, and at least by then, an effective copy construction for y should happen. The very last moment, if we do not consider the side effect of copy constructor, might be when the return value is assigned to y.

Interestingly, if I change the function template, Foo, to a regular function Bar that takes const X& and does the same thing, then, it prints the copy_con_val out:

X Bar(const X& x) {
  X out{x};
  std::cout << "out = " << out.s_ << std::endl;
  return out;
}

Are these behaviors expected, which I guess it is? If it is expected, how should I understand the differences?

CodePudding user response:

template <typename T>
T Foo(T&& t) {
  T out{std::forward<T>(t)};
  std::cout << "out = " << out.s_ << std::endl;
  return out;
}

Universal reference is the only scenario where the template type deduction actually deduces a reference type, and if you pass an lvalue, T becomes an lvalue reference (in this case X&). So in fact no copy happens in the body of the function as only references come into play. When instantiated, the function looks like this:

X& Foo(X& t) {
  X& out{ t };
  std::cout << "out = " << out.s_ << std::endl;
  return out;
}

It takes a reference and returns a reference, no extra instance was created, merely references are bound to your x variable. Later on, when a non-reference value y is assigned to a reference bound to x (after return from the function):

auto y = Foo(x);

..it actually requires another instance to come into existence and the copy constructor is called.

CodePudding user response:

With perfect forwarding, T is X&. The corrected form of Bar would be:

X& Bar(X& x) {
  X& out{x};
  std::cout << "out = " << out.s_ << std::endl;
  return out;
}

(Similar things would happen if it was a const lvalue and it was const X&)

So the copy is being made when initializing auto y = Foo(x);, where the type of y is X (not X&, since you don't have decltype(auto) y or auto& y or auto&& y), after you output out = name

Replace T with std::remove_cvref_t<T> or auto where appropriate to get your desired behaviour:

template <typename T>
std::remove_cvref_t<T> Foo(T&& t) {
  std::remove_cvref_t<T> out{std::forward<T>(t)};
  std::cout << "out = " << out.s_ << std::endl;
  return out;
}
  • Related