[英]How to allow copy elision construction for C++ classes (not just POD C structs)
考虑以下代码:
#include <iostream>
#include <type_traits>
struct A
{
A() {}
A(const A&) { std::cout << "Copy" << std::endl; }
A(A&&) { std::cout << "Move" << std::endl; }
};
template <class T>
struct B
{
T x;
};
#define MAKE_B(x) B<decltype(x)>{ x }
template <class T>
B<T> make_b(T&& x)
{
return B<T> { std::forward<T>(x) };
}
int main()
{
std::cout << "Macro make b" << std::endl;
auto b1 = MAKE_B( A() );
std::cout << "Non-macro make b" << std::endl;
auto b2 = make_b( A() );
}
这将输出以下内容:
宏制作 b
非宏make b
移动
请注意,b1 是在没有移动的情况下构建的,但 b2 的构建需要移动。
我还需要进行类型推导,因为A
在现实生活中的使用可能是一种难以明确编写的复杂类型。 我还需要能够嵌套调用(即make_c(make_b(A()))
)。
这样的 function 可能吗?
进一步的想法:
这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的:
when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target省略的复制/移动
不幸的是,这似乎我们无法将 function 参数的副本(和移动)省略到 function 结果(包括构造函数),因为这些临时对象要么绑定到引用(当通过引用传递时),要么不再是临时对象(当通过值传递时)。 在创建复合 object 时,似乎删除所有副本的唯一方法是将其创建为聚合。 但是,聚合有一定的限制,例如要求所有成员都是公共的,并且没有用户定义的构造函数。
我认为 C++ 允许对 POD C-structs 聚合构造进行优化但不允许对非 POD C++ class 构造进行相同的优化是没有意义的。
有没有办法允许非聚合构造的复制/移动省略?
我的答案:
此构造允许省略非 POD 类型的副本。 我从下面大卫罗德里格斯的回答中得到了这个想法。 它需要 C++11 lambda。 在下面的示例中,我将make_b
更改为采用两个 arguments 以使事情变得不那么琐碎。 没有调用任何移动或复制构造函数。
#include <iostream>
#include <type_traits>
struct A
{
A() {}
A(const A&) { std::cout << "Copy" << std::endl; }
A(A&&) { std::cout << "Move" << std::endl; }
};
template <class T>
class B
{
public:
template <class LAMBDA1, class LAMBDA2>
B(const LAMBDA1& f1, const LAMBDA2& f2) : x1(f1()), x2(f2())
{
std::cout
<< "I'm a non-trivial, therefore not a POD.\n"
<< "I also have private data members, so definitely not a POD!\n";
}
private:
T x1;
T x2;
};
#define DELAY(x) [&]{ return x; }
#define MAKE_B(x1, x2) make_b(DELAY(x1), DELAY(x2))
template <class LAMBDA1, class LAMBDA2>
auto make_b(const LAMBDA1& f1, const LAMBDA2& f2) -> B<decltype(f1())>
{
return B<decltype(f1())>( f1, f2 );
}
int main()
{
auto b1 = MAKE_B( A(), A() );
}
如果有人知道如何更巧妙地实现这一点,我会很感兴趣地看到它。
之前的讨论:
这在某种程度上源于对以下问题的回答:
正如 Anthony 已经提到的,该标准禁止从 function 的参数到相同 function 的返回的复制省略。 做出该决定的基本原理是复制省略(和移动省略)是一种优化,通过该优化,程序中的两个对象被合并到同一个 memory 位置,也就是说,通过使两个对象为一个来省略复制。 (部分)标准报价如下,随后是允许复制省略的一组情况,不包括该特定情况。
那么是什么让这个特殊情况有所不同呢? 区别基本上在于,在原始对象和复制对象之间存在 function 调用,而 function 调用意味着需要考虑额外的约束,特别是调用约定。
给定一个 function T foo( T )
和一个用户调用T x = foo( T(param) );
,在一般情况下,通过单独编译,编译器将在调用约定要求第一个参数所在的位置创建 object $tmp1
。 然后它将调用 function 并从 return 语句初始化x
。 这是复制省略的第一个机会:通过小心地将x
放在返回的临时位置, x
和从foo
返回的 object 变成单个 object,并且该副本被省略。 到目前为止,一切都很好。 问题是调用约定一般不会将返回的 object 和参数放在同一位置,因此$tmp1
和x
不能是 memory 中的单个位置。
在没有看到 function 定义的情况下,编译器不可能知道 function 的参数的唯一目的是用作返回语句,因此它不能省略那个额外的副本。 可以说,如果 function 是inline
的,那么编译器将缺少额外的信息来理解用于调用 function 的临时值、返回值和x
是单个 ZA8CFDE6331BD59EB26AC96F891111C4。 问题是,只有当代码实际上是内联的(不仅是标记为inline
,而且实际上是内联)时,该特定副本才能被省略。 如果标准允许在内联代码时省略该副本,则意味着程序的行为会因编译器而不是用户代码而有所不同—— inline
关键字不强制内联,它仅意味着多个定义相同的 function 不代表违反 ODR。
请注意,如果变量是在 function 内部创建的(与传入它相比),如: T foo() { T tmp; ...; return tmp; } T x = foo();
T foo() { T tmp; ...; return tmp; } T x = foo();
然后可以省略两个副本:对于必须创建tmp
的位置没有限制(它不是 function 的输入或 output 参数,因此编译器能够将其重新定位到任何地方,包括返回类型的位置,以及在调用端, x
可以像前面的例子一样小心地定位在同一个 return 语句的位置,这基本上意味着tmp
、return 语句和x
可以是单个 object。
对于您的特定问题,如果您使用宏,则代码是内联的,对对象没有限制,并且可以省略副本。 但是如果你添加一个 function,你就不能从参数中删除副本到返回语句。 所以只是避免它。 不要使用将移动object 的模板,而是创建一个将构造object 的模板:
template <typename T, typename... Args>
T create( Args... x ) {
return T( x... );
}
并且该副本可以被编译器省略。
请注意,我没有处理移动构造,因为您似乎担心移动构造的成本,即使我相信您在错误的树上吠叫。 鉴于一个激励性的真实用例,我很确定这里的人们会想出一些有效的想法。
12.8/31
当满足某些条件时,允许实现省略 class object 的复制/移动构造,即使 object 的复制/移动构造函数和/或析构函数具有副作用。 在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一 object 的两种不同方式,并且该 object 的销毁发生在两个对象本应被没有优化就被破坏了。
...但是b2的建设需要一个动作。
不,它没有。 允许编译器省略移动; 是否发生这种情况是特定于实现的,取决于几个因素。 也可以移动,但是不能复制(这种情况下必须用移动代替复制)。
确实,您不能保证移动会被忽略。 如果您必须保证不会发生移动,则使用宏或调查您的实现选项来控制此行为,特别是 function 内联。
您无法优化将A
object 从 make_b 的参数复制/移动到创建的B
make_b
的成员。
然而,这就是移动语义的全部意义——通过为A
提供轻量级的移动操作,您可以避免潜在的昂贵副本。 例如,如果A
实际上是std::vector<int>
,则可以通过使用移动构造函数来避免向量内容的复制,而只会传输内务处理指针。
这不是什么大问题。 它所需要的只是稍微改变代码的结构。
代替:
B<A> create(A &&a) { ... }
int main() { auto b = create(A()); }
你总是可以这样做:
int main() { A a; B<A> b(a); ... }
如果 B 的构造函数是这样的,那么它不会接受任何副本:
template<class T>
class B { B(T &t) :t(t) { } T &t; };
复合案例也可以:
struct C { A a; B b; };
void init(C &c) { c.a = 10; c.b = 20; }
int main() { C c; init(c); }
它甚至不需要 c++0x 特性来做到这一点。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.