简体   繁体   English

如何允许 C++ 类的复制省略构造(不仅仅是 POD C 结构)

[英]How to allow copy elision construction for C++ classes (not just POD C structs)

Consider the following code:考虑以下代码:

#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() );
}

This outputs the following:这将输出以下内容:

Macro make b宏制作 b
Non-macro make b非宏make b
Move移动

Note that b1 is constructed without a move, but the construction of b2 requires a move.请注意,b1 是在没有移动的情况下构建的,但 b2 的构建需要移动。

I also need to type deduction, as A in real life usage may be a complex type which is difficult to write explicitly.我还需要进行类型推导,因为A在现实生活中的使用可能是一种难以明确编写的复杂类型。 I also need to be able to nest calls (ie make_c(make_b(A())) ).我还需要能够嵌套调用(即make_c(make_b(A())) )。

Is such a function possible?这样的 function 可能吗?

Further thoughts:进一步的想法:

N3290 Final C++0x draft page 284: N3290 最终 C++0x 草案第 284 页:

This elision of copy/move operations, called copy elision, is permitted in the following circumstances:这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的:

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 of the omitted copy/move 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省略的复制/移动

Unfortunately this seems that we can't elide copies (and moves) of function parameters to function results (including constructors) as those temporaries are either bound to a reference (when passed by reference) or no longer temporaries (when passed by value).不幸的是,这似乎我们无法将 function 参数的副本(和移动)省略到 function 结果(包括构造函数),因为这些临时对象要么绑定到引用(当通过引用传递时),要么不再是临时对象(当通过值传递时)。 It seems the only way to elide all copies when creating a composite object is to create it as an aggregate.在创建复合 object 时,似乎删除所有副本的唯一方法是将其创建为聚合。 However, aggregates have certain restrictions, such as requiring all members be public, and no user defined constructors.但是,聚合有一定的限制,例如要求所有成员都是公共的,并且没有用户定义的构造函数。

I don't think it makes sense for C++ to allow optimizations for POD C-structs aggregate construction but not allow the same optimizations for non-POD C++ class construction.我认为 C++ 允许对 POD C-structs 聚合构造进行优化但不允许对非 POD C++ class 构造进行相同的优化是没有意义的。

Is there any way to allow copy/move elision for non-aggregate construction?有没有办法允许非聚合构造的复制/移动省略?

My answer:我的答案:

This construct allows for copies to be elided for non-POD types.此构造允许省略非 POD 类型的副本。 I got this idea from David Rodríguez's answer below.我从下面大卫罗德里格斯的回答中得到了这个想法。 It requires C++11 lambdas.它需要 C++11 lambda。 In this example below I've changed make_b to take two arguments to make things less trivial.在下面的示例中,我将make_b更改为采用两个 arguments 以使事情变得不那么琐碎。 There are no calls to any move or copy constructors.没有调用任何移动或复制构造函数。

#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() );
}

If anyone knows how to achieve this more neatly I'd be quite interested to see it.如果有人知道如何更巧妙地实现这一点,我会很感兴趣地看到它。

Previous discussion:之前的讨论:

This somewhat follows on from the answers to the following questions:这在某种程度上源于对以下问题的回答:

Can creation of composite objects from temporaries be optimised away? 可以优化从临时对象创建复合对象吗?
Avoiding need for #define with expression templates 使用表达式模板避免#define
Eliminating unnecessary copies when building composite objects 在构建复合对象时消除不必要的副本

As Anthony has already mentioned, the standard forbids copy elision from the argument of a function to the return of the same function.正如 Anthony 已经提到的,该标准禁止从 function 的参数到相同 function 的返回的复制省略。 The rationale that drives that decision is that copy elision (and move elision) is an optimization by which two objects in the program are merged into the same memory location, that is, the copy is elided by having both objects be one.做出该决定的基本原理是复制省略(和移动省略)是一种优化,通过该优化,程序中的两个对象被合并到同一个 memory 位置,也就是说,通过使两个对象为一个来省略复制。 The (partial) standard quote is below, followed by a set of circumstances under which copy elision is allowed, which do not include that particular case. (部分)标准报价如下,随后是允许复制省略的一组情况,不包括该特定情况。

So what makes that particular case different?那么是什么让这个特殊情况有所不同呢? The difference is basically that the fact that there is a function call between the original and the copied objects, and the function call implies that there are extra constraints to consider, in particular the calling convention.区别基本上在于,在原始对象和复制对象之间存在 function 调用,而 function 调用意味着需要考虑额外的约束,特别是调用约定。

Given a function T foo( T ) , and a user calling T x = foo( T(param) );给定一个 function T foo( T )和一个用户调用T x = foo( T(param) ); , in the general case, with separate compilation, the compiler will create an object $tmp1 in the location that the calling convention requires the first argument to be. ,在一般情况下,通过单独编译,编译器将在调用约定要求第一个参数所在的位置创建 object $tmp1 It will then call the function and initialize x from the return statement.然后它将调用 function 并从 return 语句初始化x Here is the first opportunity for copy elision: by carefully placing x on the location where the returned temporary is, x and the returned object from foo become a single object, and that copy is elided.这是复制省略的第一个机会:通过小心地将x放在返回的临时位置, x和从foo返回的 object 变成单个 object,并且该副本被省略。 So far so good.到目前为止,一切都很好。 The problem is that the calling convention in general will not have the returned object and the parameter in the same location, and because of that, $tmp1 and x cannot be a single location in memory.问题是调用约定一般不会将返回的 object 和参数放在同一位置,因此$tmp1x不能是 memory 中的单个位置。

Without seeing the function definition the compiler cannot possibly know that the only purpose of the argument to the function is to serve as return statement, and as such it cannot elide that extra copy.在没有看到 function 定义的情况下,编译器不可能知道 function 的参数的唯一目的是用作返回语句,因此它不能省略那个额外的副本。 It can be argued that if the function is inline then the compiler would have the missing extra information to understand that the temporary used to call the function, the returned value and x are a single object.可以说,如果 function 是inline的,那么编译器将缺少额外的信息来理解用于调用 function 的临时值、返回值和x是单个 ZA8CFDE6331BD59EB26AC96F891111C4。 The problem is that that particular copy can only be elided if the code is actually inlined (not only if it is marked as inline but actually inlined ) If a function call is required, then the copy cannot be elided.问题是,只有当代码实际上是内联的(不仅是标记为inline ,而且实际上是内联)时,该特定副本才能被省略。 If the standard allowed that copy to be elided when the code is inlined, it would imply that the behavior of a program would differ due to the compiler and not user code --the inline keyword does not force inlining, it only means that multiple definitions of the same function do not represent a violation of the ODR.如果标准允许在内联代码时省略该副本,则意味着程序的行为会因编译器而不是用户代码而有所不同—— inline关键字不强制内联,它仅意味着多个定义相同的 function 不代表违反 ODR。

Note that if the variable was created inside the function (as compared to passed into it) as in: T foo() { T tmp; ...; return tmp; } T x = foo();请注意,如果变量是在 function 内部创建的(与传入它相比),如: T foo() { T tmp; ...; return tmp; } T x = foo(); T foo() { T tmp; ...; return tmp; } T x = foo(); then both copies can be elided: There is no restriction as of where tmp has to be created (it is not an input or output parameter to the function so the compiler is able to relocate it anywhere, including the location of the returned type, and on the calling side, x can as in the previous example be carefully located in the location of that same return statement, which basically means that tmp , the return statement and x can be a single object.然后可以省略两个副本:对于必须创建tmp的位置没有限制(它不是 function 的输入或 output 参数,因此编译器能够将其重新定位到任何地方,包括返回类型的位置,以及在调用端, x可以像前面的例子一样小心地定位在同一个 return 语句的位置,这基本上意味着tmp 、return 语句和x可以是单个 object。

As of your particular problem, if you resort to a macro, the code is inlined, there are no restrictions on the objects and the copy can be elided.对于您的特定问题,如果您使用宏,则代码是内联的,对对象没有限制,并且可以省略副本。 But if you add a function, you cannot elide the copy from the argument to the return statement.但是如果你添加一个 function,你就不能从参数中删除副本到返回语句。 So just avoid it.所以只是避免它。 Instead of using a template that will move the object, create a template that will construct an object:不要使用将移动object 的模板,而是创建一个将构造object 的模板:

template <typename T, typename... Args>
T create( Args... x ) {
   return T( x... );
}

And that copy can be elided by the compiler.并且该副本可以被编译器省略。

Note that I have not dealt with move construction, as you seem concerned on the cost of even move construction, even though I believe that you are barking at the wrong tree.请注意,我没有处理移动构造,因为您似乎担心移动构造的成本,即使我相信您在错误的树上吠叫。 Given a motivating real use case, I am quite sure that people here will come up with a couple of efficient ideas.鉴于一个激励性的真实用例,我很确定这里的人们会想出一些有效的想法。

12.8/31 12.8/31

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects.当满足某些条件时,允许实现省略 class object 的复制/移动构造,即使 object 的复制/移动构造函数和/或析构函数具有副作用。 In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一 object 的两种不同方式,并且该 object 的销毁发生在两个对象本应被没有优化就被破坏了。

... but the construction of b2 requires a move. ...但是b2的建设需要一个动作。

No, it doesn't.不,它没有。 The compiler is allowed to elide the move;允许编译器省略移动; whether that happens is implementation-specific, depending on several factors.是否发生这种情况是特定于实现的,取决于几个因素。 It is also allowed to move, but it cannot copy (moving must be used instead of copying in this situation).也可以移动,但是不能复制(这种情况下必须用移动代替复制)。

It is true that you are not guaranteed that the move will be elided.确实,您不能保证移动被忽略。 If you must be guaranteed that no move will occur, then either use the macro or investigate your implementation's options to control this behavior, particularly function inlining.如果您必须保证不会发生移动,则使用宏或调查您的实现选项来控制此行为,特别是 function 内联。

You cannot optimize out the copy/move of the A object from the parameter of make_b to the member of the created B object.您无法优化将A object 从 make_b 的参数复制/移动到创建的B make_b的成员。

However, this is the whole point of move semantics --- by providing a light-weight move operation for A you can avoid a potentially expensive copy.然而,这就是移动语义的全部意义——通过为A提供轻量级的移动操作,您可以避免潜在的昂贵副本。 eg if A was actually std::vector<int> , then the copying of the vector's contents can be avoided by use of the move constructor, and instead just the housekeeping pointers will be transferred.例如,如果A实际上是std::vector<int> ,则可以通过使用移动构造函数来避免向量内容的复制,而只会传输内务处理指针。

This isn't a big problem.这不是什么大问题。 All it needs is changing the structure of the code slightly.它所需要的只是稍微改变代码的结构。

Instead of:代替:

B<A> create(A &&a) { ... }
int main() { auto b = create(A()); }

You can always do:你总是可以这样做:

int main() { A a; B<A> b(a); ... }

If the constructor of B is like this, then it'll not take any copies:如果 B 的构造函数是这样的,那么它不会接受任何副本:

template<class T>
class B { B(T &t) :t(t) { } T &t; };

The composite case will work too:复合案例也可以:

struct C { A a; B b; };
void init(C &c) { c.a = 10; c.b = 20; }
int main() { C c; init(c); } 

And it doesn't even need c++0x features to do this.它甚至不需要 c++0x 特性来做到这一点。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM