作者:手机用户2502884005 | 来源:互联网 | 2022-12-03 19:58
我试图理解完美转发和构造函数的相互作用.我的例子如下:
#include
#include
template
using disable_if_same_or_derived =
std::enable_if_t<
!std::is_base_of<
A,
std::remove_reference_t
>::value
>;
template
class wrapper {
public:
// perfect forwarding ctor in order not to copy or move if unnecessary
template<
class T0,
class = disable_if_same_or_derived // do not use this instead of the copy ctor
> explicit
wrapper(T0&& x)
: x(std::forward(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 w_1 {t1}; // prints "copy ctor": OK
trace t2;
wrapper w_2 {std::move(t2)}; // prints "move ctor": OK
wrapper w_3 {trace()}; // prints "move ctor": why?
}
我希望我wrapper
没有任何开销.特别是,当将一个临时编组到包装器中时w_3
,我希望该trace
对象可以直接在适当的位置创建,而不必调用移动ctor.但是,有一个移动ctor调用,这使我认为临时创建然后移动.移动ctor为什么叫?怎么不打电话呢?
1> bolov..:
我希望跟踪对象可以直接在适当的位置创建,而无需调用移动ctor.
我不知道为什么你会这么想.转发就是这样做的:移动或复制1).在您的示例中,您创建一个临时的trace()
,然后转发将其移入x
如果要构建T
对象,则需要将参数传递给构造T
,而不是T
要移动或复制的对象.
创建一个就地构造函数:
template
wrapper(std::in_place_t, Args&&... args)
:x{std::forward(args)...}
{}
然后像这样调用它:
wrapper w_3 {std::in_place};
// or if you need to construct an `trace` object with arguments;
wrapper w_3 {std::in_place, a1, a2, a3};
在另一个答案中处理OP的评论:
@bolov让我们忘记完美转发一分钟.我认为问题在于我希望在最终目的地构建一个对象.现在,如果它不在构造函数中,现在可以保证可以使用有保证的复制/移动省略(此处移动和复制几乎相同).我不明白为什么在构造函数中这是不可能的.我的测试用例证明它不会按照现行标准发生,但我认为这不应该由标准指定并由编译器实现.我错过了什么关于ctor的特别之处?
在这方面,关于ctor绝对没有什么特别之处.您可以使用简单的自由函数查看完全相同的行为:
template
auto simple_function(T&& a)
{
X x = std::forward(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
(或者作为你的包装器对象中的数据成员),你需要理解保证copy 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
现在被移动(因为它是一个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 wrapper {
public:
template
wrapper(F initializer)
: x{initializer()}
{}
private:
T x;
};
auto test()
{
wrapper w = [] { return X{};};
}
在C++ 17中,即使X
已删除了复制构造函数和移动构造函数,也不会复制和/或移动它.对象将在它的最终目的地构建,就像你想要的那样.
1)如果使用得当,我正在谈论转发习语.std::forward
只是一个演员.