3

I am trying to understand the interaction of perfect forwarding and constructors. My example is the following:

#include <utility>
#include <iostream>


template<typename A, typename B>
using disable_if_same_or_derived =
  std::enable_if_t<
    !std::is_base_of<
      A,
      std::remove_reference_t<B>
    >::value
  >;


template<class T>
class wrapper {
  public:
    // perfect forwarding ctor in order not to copy or move if unnecessary
    template<
      class T0,
      class = disable_if_same_or_derived<wrapper,T0> // do not use this instead of the copy ctor
    > explicit
    wrapper(T0&& x)
      : x(std::forward<T0>(x))
    {}

  private:
    T x;
};


class trace {
  public:
    trace() {}
    trace(const trace&) { std::cout << "copy ctor\n"; }
    trace& operator=(const trace&) { std::cout << "copy assign\n"; return *this; }
    trace(trace&&) { std::cout << "move ctor\n"; }
    trace& operator=(trace&&) { std::cout << "move assign\n"; return *this; }
};


int main() {
  trace t1;
  wrapper<trace> w_1 {t1}; // prints "copy ctor": OK

  trace t2;
  wrapper<trace> w_2 {std::move(t2)}; // prints "move ctor": OK

  wrapper<trace> w_3 {trace()}; // prints "move ctor": why?
}

I want my wrapper to have no overhead at all. In particular, when marshalling a temporary into the wrapper, as with w_3, I would expect the trace object to be created directly in place, without having to call the move ctor. However, there is a move ctor call, which makes me think a temporary is created and then moved from. Why is the move ctor called? How not to call it?

Bérenger
  • 2,678
  • 2
  • 21
  • 42
  • You seem to expect the compiler to magically construct the temporary directly in the spot of `&w_3.x`. I don't think that can happen. There is such a thing as return value optimization, but no such thing as argument optimization. In other words, in `wrapper` constructor, `&x != &(this->x)`. You can do something like `std::vector::emplace`- have a constructor that takes arbitrary arguments, and passes them along to constructor of `x`. – Igor Tandetnik Sep 25 '18 at 12:54
  • @IgorTandetnik Yes I would expect that and I don't completly see the difference with RVO. But even without optimizations, I don't even understand why there is a need for a move ctor. For me, a move ctor move an lvalue from one place to another. And here I only have an rvalue and I am careful of keeping the rvalue-ness through std::forward. What is happening? – Bérenger Sep 25 '18 at 13:01
  • 1
    The argument is an rvalue - but the ultimate destination, `w_3.x`, is an lvalue. Between the two, some form of copying or moving must occur. – Igor Tandetnik Sep 25 '18 at 13:15
  • "_For me, a move ctor move an lvalue from one place to another._" That's wrong. The copy c'tor copies an l-value; the move c'tor moves from an r-value. – D Drmmr Sep 26 '18 at 08:14

2 Answers2

6

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.

bolov
  • 72,283
  • 15
  • 145
  • 224
  • `Forwarding does exactly this: moves or copies` That is what I don't understand. Are you saying it in general or in this context? Because for me, except in the ctor, it does neither copy or move, it forwards the arguments. Now I am confused with the ctor: why is it different? Why isn't the object forwarded to &w_3.x? – Bérenger Sep 25 '18 at 13:10
  • @Bérenger What is "forwarded to `&w_3.x`" supposed to mean? Do you expect some kind of immaculate transfer, by which the object is magically shifted in memory and changes its address without being copied or moved? – Igor Tandetnik Sep 25 '18 at 13:18
  • @IgorTandetnik Ok I think I understand thanks to your comment of the OP. What is a little bit tricky is that you are talking about the address of a temporary. But a temporary has no address right? So I would have expected the compiler, when seeing a temporary, to not construct it until it is moved to an lvalue. However, this seems to not be the case right? :it has no idea when it will be moved, so it has to create it somewhere, and then move it later, e.g. when asked to by the ctor. – Bérenger Sep 25 '18 at 13:24
  • @Bérenger Temporary does have an address. You can print `&x` in `wrapper` constructor. It's true that you cannot write `&(trace())` - but that doesn't mean `trace()` doesn't occupy memory; it's more of a syntactical limitation. You can take an address of a temporary via a reference bound to it, or you could have a method that returns `this`, and call it as in `trace().GetThis()` – Igor Tandetnik Sep 25 '18 at 13:33
  • @Bérenger technically `st::forward` is a cast to an `lvalue` or an `xvalue`. If it results in an `lvalue` the result can be copied from, if the result is an `xvalue` then the result can be moved from. – bolov Sep 25 '18 at 14:14
  • @IgorTandetnik From my understanding of xskxzr answer, the "kind of immaculate transfer" would be to bind a rvalue reference to an expression instead of an object. At first sight, I think it should be doable – Bérenger Sep 25 '18 at 14:36
  • 1
    @Bérenger That answer doesn't make any sense to me. `int&& r = 1+2;` is perfectly valid. I'm not sure what "bind a reference to an expression" means, nor how it relates to the problem at hand, nor what the cited DR2327 has to do with anything. – Igor Tandetnik Sep 25 '18 at 14:44
  • @IgorTandetnik `int f() { return 1+2; }` is now garanteed to not create a temporary then copy/move, but construct the result in place. So in a way, `int i = f()` binds `&i` to the expression `f()`. Regarding the constructor, the equavalent would be: `wrapper(T0&& x0) : x(std::forward(x0)) {}` where `x0` is not really an object but the expression leading to that object. And then `x` is constructed in place from the expression of `x0`. – Bérenger Sep 25 '18 at 16:58
  • @IgorTandetnik Well I don't know. Presenting it like that, I am starting to think its not trivial at all, because the compiler may not have access to the expression that leads to the temporary. – Bérenger Sep 25 '18 at 17:02
  • @bolov Thanks for the detailed answer. "once you have named something, that something must exist." This is the piece I missed. I am indeed a bit reluctant to pass arguments of ctor of T to ctor of wrapper because I'm not sure that wrapper should know how to construct a T. But still better than a lambda I think. – Bérenger Sep 25 '18 at 19:25
  • @Bérenger this practice is used in the standard. e.g. `std::vector::emplace_back` `std::optional::optional( std::in_place_t, Args&&... args )` etc. You can use it with no worries. – bolov Sep 25 '18 at 19:30
  • @Bérenger glad to have found the thing that clicked with you. You are welcome. – bolov Sep 25 '18 at 19:32
  • Note: I don't see how the in-place method could be used for more complex examples. For instance for std::tuple: how do you marshall each Args&&... args needed for each type of the tuple? – Bérenger Sep 25 '18 at 20:24
  • The standard library seems to be as embarrassed as me since the std::inplace used for variant/optional does not have an equivalent for std::pair or std::tuple – Bérenger Sep 25 '18 at 20:26
  • `std::in_place_t` does not solve the problem with non-empty arguments because prvalue arguments are still forwarded as xvalues. – xskxzr Sep 26 '18 at 03:52
  • @xskxzr but you are talking about the arguments passed to the constructor. The object is constructed in place with no copy/move. – bolov Sep 26 '18 at 11:50
0

A reference (either lvalue reference or rvalue reference) must be bound to an object, so when the reference parameter x is initialized, a temporary object is required to be materialized anyway. In this sense, perfect forwarding is not that "perfect".

Technically, to elide this move, the compiler must know both the initializer argument and the definition of the constructor. This is impossible because they may lie in different translation units.

xskxzr
  • 12,442
  • 12
  • 37
  • 77
  • Hum... So I am correct if I say that the garanteed copy elision introduced in c++17 should and can be extended to ctors (which would solve my unnecessary move problem), and that the reason it is not the case is that the commitee has to figure out how to do it properly without further limitations? – Bérenger Sep 25 '18 at 14:32
  • @Bérenger Right. – xskxzr Sep 25 '18 at 14:34
  • Ok i'll accept the answer. If somebody stumble upon it 5 years from now, please update the status of the standard, – Bérenger Sep 25 '18 at 14:41
  • I don't think DR2327 applies. `wrapper w_3 {trace()}` is not a copy initialization; there is no hope for copy elision here. – Igor Tandetnik Sep 25 '18 at 14:43
  • the core issue is about not considering operator conversions along constructors in initialization. It has nothing to do with the issue at hand here. – bolov Sep 25 '18 at 14:52
  • @Bérenger @xskxzr no, mandatory copy elision introduced in C++17 wouldn't apply here or anywhere where `std::forward` is because `std::forward` hides the `prvalue` behind an `xvalue` – bolov Sep 25 '18 at 14:55
  • @bolov Emm... you are right, I didn't read that issue carefully. I hope perfect forwarding can be improved but that issue itself is irrelevant. – xskxzr Sep 25 '18 at 15:07
  • 1
    forwarding cannot be improved because it does exactly as advertised. It addresses the issue that named lvalue references, named rvalue references and named forwarding references are all lvalues and casts a named lvalue reference to an lvalue and a named rvalue reference (which is an lvalue) to an xvalue so that further code can differentiate between the categories. You would need a completely different concept to do what you expect. – bolov Sep 25 '18 at 15:18
  • @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? – Bérenger Sep 25 '18 at 16:42
  • @bolov Even if xskxzr link was not directly linked to this problem, I still think this is somehow linked to garanteed move/copy elision. Am I wrong? – Bérenger Sep 25 '18 at 16:50
  • @Bérenger I updated my question to answer your question – bolov Sep 25 '18 at 18:13
  • @Bérenger I edited my answer. I hope it makes sense now. – xskxzr Sep 26 '18 at 05:00
  • @xskxzr Yes I understand. Actually, it would be possible with all inline functions (not only template). – Bérenger Sep 26 '18 at 09:08