简体   繁体   English

完美的转发和构造函数

[英]Perfect forwarding and constructors

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. 我希望我的wrapper完全没有开销。 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. 特别是,编组临时进入包装时,与w_3 ,我期望trace对象到位直接创建,而不必调用构造函数的举动。 However, there is a move ctor call, which makes me think a temporary is created and then moved from. 但是,有一个move ctor调用,这使我认为创建了一个临时对象,然后将其从中移出。 Why is the move ctor called? 为什么叫移动ctor? How not to call it? 怎么不叫呢?

I would expect the trace object to be created directly in place, without having to call the move ctor. 我希望可以直接在适当位置创建跟踪对象,而不必调用move ctor。

I don't know why you expect that. 我不知道你为什么这么期望。 Forwarding does exactly this: moves or copies 1) . 转发正是这样做的:移动或复制1) In your example you create a temporary with trace() and then the forwarding moves it into x 在您的示例中,使用trace()创建一个临时目录,然后转发将其移至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. 如果要在适当位置构造T对象,则需要将参数传递给T的构造,而不是要移动或复制的T对象。

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: 解决OP对另一个答案的评论:

@bolov Lets forget perfect forwarding for a minute. @bolov让我们暂时忘记完美的转发。 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? 我想念ctor这么特别的地方是什么?

There is absolutely nothing special about a ctor in this regard. 在这方面,ctor绝对没有什么特别的。 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. 上面的示例与您的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 . 您可以看到simple_function类似于包装器构造函数,而我的本地x变量类似于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 . 为了理解为什么你不能在本地范围内直接构造对象simple_function (或在你的情况你的包装对象数据成员),你需要了解保证复制省略如何在C ++ 17我推荐这个外观极好回答

To sum up that answer: basically a prvalue expression doesn't materializez an object, instead it is something that can initialize an object. 总结一下答案:基本上,一个prvalue表达式不会实现一个对象,而是可以初始化一个对象的东西。 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. 使用表达式初始化simple_foo的参数(或构造函数的参数)的那一刻,您被迫具体化一个对象并丢失了表达式。 From now on you don't have the original prvalue expression anymore, you have a created materialized object. 从现在开始,您不再具有原始的prvalue表达式,而是创建了一个物化对象。 And this object now needs to be moved into your final destination - my local x (or your data member x ). 现在,该对象需要移动到您的最终目标位置-我的本地x (或您的数据成员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 . X{}创建一个临时对象作为simple_function参数。 Lets call it Temp1 让我们称之为Temp1
  • Temp1 is now moved (because it is a prvalue) into the parameter a of simple_function 现在将Temp1 (因为它是prvalue)移到simple_function的参数a
  • a is copied (because a is an lvalue) into x a复制(因为a为左值)到x
  • a is moved (because std::move casts a to an xvalue) to x2 a移动(因为std::movea强制转换为xvalue)到x2

Now with C++17 guaranteed copy elision 现在使用C ++ 17保证复制省略

  • X{} no longer materializez an object on the spot. X{}不再当场实现对象。 Instead the expression is held onto. 而是保留表达式。
  • the parameter a of simple_function can now by initialized from the X{} expression. 现在可以通过X{}表达式初始化simple_function的参数a No copy or move involved nor required. 无需复制或移动,也不需要。

The rest is now the same: 其余部分现在相同:

  • a is copied into x1 a复制到x1
  • a is moved into x2 a移入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 . 您已将wrapper::wrapper的参数命名为。 I have named the parameter of simple_function . 我已将参数命名为simple_function That is the moment you lose your prvalue expression to initialize that named object. 那是您丢失prvalue表达式以初始化该命名对象的时刻。


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. 如果要使用C ++ 17保证的复制省略,而又不喜欢就地方法,则需要避免命名:)可以使用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. 由于我还没有在野外看到过lambda方式,所以我不知道是否会推荐它。 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. 在C ++ 17中,此被授予者不进行任何复制和/或移动,并且即使X删除了复制构造函数和move构造函数,它也可以工作。 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. 1)我说的是正确使用的转发习语。 std::forward is just a cast. std::forward只是演员表。

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. 引用(左值引用或右值引用)必须绑定到对象,因此在初始化引用参数x ,无论如何都需要实现一个临时对象。 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. 这是不可能的,因为它们可能位于不同的翻译单元中。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM