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;
}