完美的转发和构造函数

时间:2018-09-25 12:50:35

标签: c++ perfect-forwarding temporary-objects

我试图了解完美转发和构造函数之间的相互作用。我的示例如下:

#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?
}

我希望我的wrapper完全没有开销。特别是,当像w_3那样将临时文件编组到包装器中时,我希望trace对象可以直接就地创建,而不必调用move ctor。但是,有一个move ctor调用,这使我认为创建了一个临时对象,然后将其从中移出。为什么叫移动ctor?怎么不叫呢?

2 个答案:

答案 0 :(得分:5)

  

我希望可以直接在原位创建跟踪对象,   无需致电ctor。

我不知道你为什么这么期望。转发正是这样做的:移动或复制 1)。在您的示例中,您使用trace()创建了一个临时目录,然后转发将其移至x

如果要就地构造T对象,则需要将参数传递给T的构造,而不是要移动或复制的T对象。

创建就地构造函数:

template <class... Args>
wrapper(std::in_place_t, Args&&... args)
    :x{std::forward<Args>(args)...}
{}

然后这样称呼它:

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

处理来自OP的关于另一个答案的评论:

  

@bolov让我们暂时忘记完美的转发。我觉得   问题是我希望最终构造一个对象   目的地。现在,如果它不在构造函数中,则将被保证   发生在保证的复制/移动省略(此处移动和复制是   差不多一样)。我不明白的是为什么不会这样   可能在构造函数中。我的测试案例证明这没有发生   根据目前的标准,但我不认为这应该是   无法由标准指定并由编译器实现。什么   我想念有关ctor的特别之处吗?

在这方面,ctor绝对没有什么特别的。您可以通过简单的免费功能看到完全相同的行为:

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

以上示例与您的OP相似。您可以在包装simple_function中看到x类似于包装构造函数,而本地wrapper则类似于您的数据成员。在这方面的机制是相同的。

为了理解为什么不能直接在simple_function的本地范围内构造对象(或作为包装对象中的数据成员),需要了解如何保证在C中使用复制省略++ 17我推荐this excelent answer

总结这个答案:基本上,一个prvalue表达式不会实现一个对象,而是可以初始化一个对象的东西。在将其用于初始化对象之前,请尽可能长时间地保留该表达式(从而避免某些复制/移动)。请参阅链接的答案,以得到更深入而友好的解释。

在您的表达式用于初始化simple_foo的参数(或构造函数的参数)的那一刻,您被迫具体化一个对象并失去了表达式。从现在开始,您不再具有原始的prvalue表达式,而是创建了一个物化对象。现在,该对象需要移动到您的最终目的地-我的本地x(或您的数据成员x)。

如果我们稍微修改一下示例,我们可以看到在工作中保证了复制省略:

auto simple_function(X a)
{
    X x = a;
    X x2 = std::move(a);
}


auto test()
{
    simple_function(X{});
}

如果没有省略,事情会像这样:

  • X{}创建一个临时对象作为simple_function的参数。让我们称之为Temp1
  • Temp1现在(由于是prvalue)被移到a的参数simple_function
  • a被复制(因为a是左值)到x
  • a已移动(因为std::movea转换为xvalue)移动到x2

现在具有C ++ 17保证的复制省略

  • X{}不再当场实现对象。而是保留表达式。
  • 现在可以通过a表达式初始化simple_function的参数X{}。不需要复制或移动。

其余部分相同:

  • a被复制到x1
  • a已移至x2

您需要了解的内容:一旦命名了某个东西,该东西必须存在。令人惊讶的简单原因是,一旦您为某件事取了个名字,就可以多次引用它。请参阅我对此other question的回答。您已将参数命名为wrapper::wrapper。我将参数命名为simple_function。那是您丢失prvalue表达式以初始化该命名对象的时刻。


如果要使用C ++ 17保证的复制省略,而又不喜欢就地方法,则需要避免命名:)可以使用lambda来实现。我最常看到的成语是就地方式,包括在标准中。由于我还没有在野外看到过lambda方式,所以我不知道是否会推荐它。无论如何,这里是

template<class T> class wrapper {
public:

    template <class F>
    wrapper(F initializer)
        : x{initializer()}
    {}

private:
    T x;
};

auto test()
{
    wrapper<X> w = [] { return X{};};
}

在C ++ 17中,此被授予者不进行任何复制和/或移动,并且即使X删除了复制构造函数和move构造函数也可以工作。就像您想要的那样,将在最终目的地构造对象。


1)我说的是正确使用转发惯用法。 std::forward只是演员表。

答案 1 :(得分:0)

必须将引用(左值引用或右值引用)绑定到对象,因此在初始化引用参数x时,无论如何都需要实现一个临时对象。从这个意义上说,完美的转发并不是那么“完美”。

从技术上讲,要避免此举,编译器必须同时知道初始化程序参数和构造函数的定义。这是不可能的,因为它们可能位于不同的翻译单元中。