I would expect the trace object to be created directly in place,
without having to call the move ctor.
I don't know why you expect that. Forwarding does exactly this: moves or copies 1). In your example you create a temporary with trace()
and then the forwarding moves it into x
If you want to construct a T
object in place then you need to pass the arguments to the construction of T
, not an T
object to be moved or copied.
Create an in place constructor:
template <class... Args>
wrapper(std::in_place_t, Args&&... args)
:x{std::forward<Args>(args)...}
{}
And then call it like this:
wrapper<trace> w_3 {std::in_place};
// or if you need to construct an `trace` object with arguments;
wrapper<trace> w_3 {std::in_place, a1, a2, a3};
Addressing a comment from the OP on another answer:
@bolov Lets forget perfect forwarding for a minute. I think the
problem is that I want an object to be constructed at its final
destination. Now if it is not in the constructor, it is now garanteed
to happen with the garanteed copy/move elision (here move and copy are
almost the same). What I don't understand is why this would not be
possible in the constructor. My test case proves it does not happen
according to the current standard, but I don't think this should be
impossible to specify by the standard and implement by compilers. What
do I miss that is so special about the ctor?
There is absolutely nothing special about a ctor in this regard. You can see the exact same behavior with a simple free function:
template <class T>
auto simple_function(T&& a)
{
X x = std::forward<T>(a);
// ^ guaranteed copy or move (depending on what kind of argument is provided
}
auto test()
{
simple_function(X{});
}
The above example is similar with your OP. You can see simple_function
as analog to your wrapper constructor and my local x
variable as analog to your data member in wrapper
. The mechanism is the same in this regard.
In order to understand why you can't construct the object directly in the local scope of simple_function
(or as data member in your wrapper object in your case) you need to understand how guaranteed copy elision works in C++17 I recommend this excelent answer.
To sum up that answer: basically a prvalue expression doesn't materializez an object, instead it is something that can initialize an object. You hold on to the expression for as long as possible before you use it to initialize an object (thus avoiding some copy/moves). Refer to the linked answer for a more in-depth yet friendly explanation.
The moment your expression is used to initialize the parameter of simple_foo
(or the parameter of your constructor) you are forced to materialize an object and lose your expression. From now on you don't have the original prvalue expression anymore, you have a created materialized object. And this object now needs to be moved into your final destination - my local x
(or your data member x
).
If we modify my example a bit we can see guaranteed copy elision at work:
auto simple_function(X a)
{
X x = a;
X x2 = std::move(a);
}
auto test()
{
simple_function(X{});
}
Without elision things would go like this:
X{}
creates a temporary object as argument for simple_function
. Lets call it Temp1
Temp1
is now moved (because it is a prvalue) into the parameter a
of simple_function
a
is copied (because a
is an lvalue) into x
a
is moved (because std::move
casts a
to an xvalue) to x2
Now with C++17 guaranteed copy elision
X{}
no longer materializez an object on the spot. Instead the expression is held onto.
- the parameter
a
of simple_function
can now by initialized from the X{}
expression. No copy or move involved nor required.
The rest is now the same:
a
is copied into x1
a
is moved into x2
What you need to understand: once you have named something, that something must exist. The surprisingly simple reason for that is that once you have a name for something you can reference it multiple times. See my answer on this other question. You have named the parameter of wrapper::wrapper
. I have named the parameter of simple_function
. That is the moment you lose your prvalue expression to initialize that named object.
If you want to use the C++17 guaranteed copy elision and you don't like the in-place method you need to avoid naming things :) You can do that with a lambda. The idiom I see most often, including in the standard, is the in-place way. Since I haven't seen the lambda way in the wild, I don't know if I would recommend it. Here it is anyway:
template<class T> class wrapper {
public:
template <class F>
wrapper(F initializer)
: x{initializer()}
{}
private:
T x;
};
auto test()
{
wrapper<X> w = [] { return X{};};
}
In C++17 this grantees no copies and/or moves and that it works even if X
has deleted copy constructors and move constructors. The object will be constructed at it's final destination, just like you want.
1) I am talking about the forwarding idiom, when used properly. std::forward
is just a cast.