我试图了解完美转发和构造函数之间的相互作用。我的示例如下:
#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?怎么不叫呢?
答案 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::move
将a
转换为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
时,无论如何都需要实现一个临时对象。从这个意义上说,完美的转发并不是那么“完美”。
从技术上讲,要避免此举,编译器必须同时知道初始化程序参数和构造函数的定义。这是不可能的,因为它们可能位于不同的翻译单元中。