c++ - 完美的转发和构造函数
问题描述
我试图了解完美转发和构造函数的交互。我的例子如下:
#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?怎么不叫呢?
解决方案
我希望直接在原地创建跟踪对象,而不必调用移动 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)到参数a
中simple_function
a
被复制(因为a
是左值)到x
a
被移动(因为std::move
强制转换a
为 xvalue)到x2
现在使用 C++17 保证复制省略
X{}
不再当场具体化一个物体。相反,表达式被保留。- 现在可以从表达式初始化参数
a
。不涉及复制或移动,也不需要。simple_function
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
只是一个演员表。