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

Cli*_*ton 28 c++ macros optimization c++11

请考虑以下代码:

#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() );
}
Run Code Online (Sandbox Code Playgroud)

这输出如下:

宏制作b
非宏制作b
移动

注意,b1是在没有移动的情况下构造的,但是b2的构造需要移动.

我还需要输入演绎,因为A在现实生活中使用可能是一种难以明确写出的复杂类型.我还需要能够嵌套调用(即make_c(make_b(A()))).

这样的功能可能吗?

进一步思考:

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

在下列情况下,允许复制/移动操作的省略,称为复制省略:

当一个未绑定到引用(12.2)的临时类对象被复制/移动到具有相同cv-nonqualified类型的类对象时,可以通过将临时对象直接构造到目标中来省略复制/移动操作省略的复制/移动

不幸的是,我们不能忽略函数参数(包括构造函数)的复制(和移动),因为那些临时函数要么绑定到引用(通过引用传递),要么不再是临时值(通过值传递时).在创建复合对象时,忽略所有副本的唯一方法是将其创建为聚合.但是,聚合具有某些限制,例如要求所有成员都是公共的,并且没有用户定义的构造函数.

我认为C++不允许优化POD C-structs聚合构造,但不允许对非POD C++类构造进行相同的优化.

有没有办法允许复制/移动省略用于非聚合构造?

我的答案:

此构造允许为非POD类型省略副本.我从DavidRodríguez的答案中得到了这个想法.它需要C++ 11 lambda.在下面的这个例子中,我改变make_b了两个参数,使事情变得不那么重要.没有任何移动或复制构造函数的调用.

#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() );
}
Run Code Online (Sandbox Code Playgroud)

如果有人知道如何更整洁地实现这一点,我会非常有兴趣看到它.

前面的讨论:

这有点紧接着以下问题的答案:

可以优化临时创建复合对象吗?
使用表达式模板避免#define的需要
在构建复合对象时消除不必要的副本

Dav*_*eas 9

正如安东尼已经提到的那样,标准禁止从函数的参数复制省略到返回相同的函数.驱动该决定的基本原理是复制省略(和移动省略)是一种优化,通过该优化,程序中的两个对象被合并到同一存储器位置,即,通过使两个对象都是一个来省略复制.(部分)标准引用如下,其后是允许复制省略的一系列情况,其中不包括该特定情况.

那么是什么让那个特例不同呢?区别在于原始对象和复制对象之间存在函数调用,而函数调用意味着需要考虑额外的约束,特别是调用约定.

给定一个函数T foo( T )和一个用户调用T x = foo( T(param) );,在一般情况下,使用单独的编译,编译器将$tmp1在调用约定要求第一个参数的位置创建一个对象.然后它将调用该函数并x从return语句初始化.这是复制省略的第一次机会:通过小心地放置x返回的临时x对象的位置,并且返回的对象foo成为单个对象,并且该复制被省略.到现在为止还挺好.问题是调用约定一般不会将返回的对象和参数放在同一个位置,因此,$tmp1并且x不能是内存中的单个位置.

在没有看到函数定义的情况下,编译器不可能知道函数参数的唯一目的是作为return语句,因此它不能忽略那个额外的副本.可以说,如果函数是inline编译器,则会有缺少的额外信息来理解临时用于调用函数,返回值并且x是单个对象.问题是,如果代码实际上是内联特定副本只能被省略(不只是如果它被标记为inline,但实际上内联)如果需要一个函数调用,那么副本不能被省略.如果标准允许在内联代码时省略该副本,则意味着程序的行为会因编译器而不是用户代码而不同 - inline关键字不强制内联,它只表示多个定义相同的功能不代表违反ODR.

请注意,如果变量是函数内部创建的(与传入函数相比),如下所示:T foo() { T tmp; ...; return tmp; } T x = foo();那么两个副本都可以省略:tmp对于必须创建的位置没有限制(它不是输入或输出参数)函数,所以编译器能够将它重定位到任何地方,包括返回类型的位置,并且在调用方面,x可以像前面的例子一样小心地位于同一个return语句的位置,这基本上意味着tmp,返回声明并且x可以是单个对象.

至于您的特定问题,如果您使用宏,代码是内联的,对对象没有限制,可以省略副本.但是如果添加一个函数,则不能将该参数中的副本删除到return语句.所以只是避免它.不使用将移动对象的模板,而是创建将构造对象的模板:

template <typename T, typename... Args>
T create( Args... x ) {
   return T( x... );
}
Run Code Online (Sandbox Code Playgroud)

编译器可以省略该副本.

请注意,我没有处理移动构造,因为你似乎担心移动构造的成本,即使我相信你在错误的树上吠叫.鉴于一个激励性的实际用例,我很确定这里的人会提出一些有效的想法.

12.8/31

当满足某些条件时,允许实现省略类对象的复制/移动构造,即使该对象的复制/移动构造函数和/或析构函数具有副作用.在这种情况下,实现将省略的复制/移动操作的源和目标视为仅仅两种不同的引用同一对象的方式,并且该对象的销毁发生在两个对象的后期时间.没有优化就被破坏了.


Fre*_*urk 5

......但是b2的构造需要移动.

不,它没有.允许编译器忽略移动; 这是否是特定于实现的,取决于几个因素.它也被允许移动,但它无法复制(在这种情况下必须使用移动而不是复制).

确实,您无法保证此举被取消.如果必须保证不会发生任何移动,则使用宏或调查实现的选项来控制此行为,尤其是函数内联.