完美的转发和构造函数

Perfect forwarding and constructors

本文关键字:构造函数 转发 完美      更新时间:2023-10-16

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

#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 ctorn"; }
trace& operator=(const trace&) { std::cout << "copy assignn"; return *this; }
trace(trace&&) { std::cout << "move ctorn"; }
trace& operator=(trace&&) { std::cout << "move assignn"; 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。但是,有一个移动 ctor 调用,这让我认为创建了一个临时然后从中移动。为什么叫移动 ctor?怎么不叫呢?

我希望

跟踪对象直接就地创建, 无需调用移动 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 类似。您可以在wrapper中看到simple_function是包装构造函数的模拟,而我的局部x变量是数据成员的模拟。在这方面,机制是相同的。

为了理解为什么你不能直接在simple_function的本地范围内构造对象(或者在你的情况下作为包装器对象中的数据成员),你需要了解保证复制 elision 在 C++17 中是如何工作的,我推荐这个很好的答案。

总结一下这个答案:基本上,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现在被移动到参数asimple_function
  • a被复制到(因为a是一个左值)到x
  • a被移动(因为std::movea转换为 x值)到x2

现在具有 C++17 保证复制消除

  • X{}不再当场实现物体。相反,表达式被保留。
  • simple_function的参数a现在可以通过从X{}表达式初始化。不涉及也不要求复制或移动。

其余的现在是一样的:

  • 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只是一个演员。

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

从技术上讲,要避免这一移动,编译器必须知道初始值设定项参数和构造函数的定义。这是不可能的,因为它们可能位于不同的翻译单元中。