首页 > 解决方案 > 完美的转发和构造函数

问题描述

我试图了解完美转发和构造函数的交互。我的例子如下:

#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对象可以直接在适当的位置创建,而无需调用移动 ctor。但是,有一个 move ctor 调用,这让我认为创建了一个临时对象,然后从中移出。为什么叫move ctor?怎么不叫呢?

标签: c++perfect-forwardingtemporary-objects

解决方案


我希望直接在原地创建跟踪对象,而不必调用移动 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 让我们暂时忘记完美转发。我认为问题在于我希望在其最终目的地构造一个对象。现在,如果它不在构造函数中,现在保证会发生保证复制/移动省略(这里移动和复制几乎相同)。我不明白为什么这在构造函数中是不可能的。我的测试用例证明它不会按照当前的标准发生,但我认为这应该不是不可能由标准指定并由编译器实现的。我想念这个演员的特别之处吗?

在这方面,演员绝对没有什么特别之处。您可以通过一个简单的自由函数看到完全相同的行为:

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 中的工作原理,我推荐这个优秀的答案.

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

当您的表达式用于初始化simple_foo(或构造函数的参数)的参数时,您被迫物化一个对象并丢失您的表达式。从现在开始,您不再拥有原始纯右值表达式,而是创建了一个物化对象。现在需要将此对象移动到您的最终目的地 - 我的本地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)到参数asimple_function
  • a被复制(因为a是左值)到x
  • a被移动(因为std::move强制转换a为 xvalue)到x2

现在使用 C++17 保证复制省略

  • X{}不再当场具体化一个物体。相反,表达式被保留。
  • 现在可以从表达式初始化参数a。不涉及复制或移动,也不需要。simple_functionX{}

其余的现在是一样的:

  • a被复制到x1
  • a被搬进x2

您需要了解的内容:一旦您命名了某物,则该物必须存在。一个令人惊讶的简单原因是,一旦您为某事物命名,您就可以多次引用它。请参阅我对其他问题的回答。您已命名 的参数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删除了复制构造函数和移动构造函数,它也可以工作。该对象将在其最终目的地构建,就像您想要的那样。


1)我说的是转发成语,如果使用得当。std::forward只是一个演员表。


推荐阅读