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

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

本文关键字:复制省 结构 c++ 何允许 POD 不仅仅是      更新时间:2023-10-16

考虑以下代码:

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

输出如下内容:

宏make b
非宏make b

移动

注意b1的构造不需要移动,但b2的构造需要移动。

我还需要类型演绎,因为现实生活中使用的A可能是一个复杂的类型,很难显式地写出来。我还需要能够嵌套调用(即make_c(make_b(A())))。

这样的函数可能吗?

进一步的想法:

N3290 Final c++ 0x draft page 284:

拷贝/移动操作的省略,称为复制省略,是允许的以下情况:

当一个临时类对象具有未绑定到引用 (12.2)会被复制/移动到一个类吗对象具有相同的cv-不限定输入后,可进行复制/移动操作通过构造临时的来省略对象直接装入目标的省略了复制/移动

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

我不认为c++允许对POD C结构的聚合构造进行优化,但不允许对非POD c++类的构造进行相同的优化是有意义的。

是否有任何方法允许复制/移动省略非聚合结构?

我的回答:

此构造允许省略非pod类型的副本。我从下面David Rodrí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() );
}

如果有人知道如何更整齐地实现这一点,我很有兴趣看看。

先前的讨论:

这在某种程度上是由对以下问题的回答而来的:

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

正如Anthony已经提到的,标准禁止从函数的实参到同一函数的返回的复制省略。驱动这个决定的基本原理是,复制省略(和移动省略)是一种优化,通过这种优化,程序中的两个对象被合并到相同的内存位置,也就是说,通过使两个对象为一个来省略复制。(部分)标准引用如下,后面是一组允许副本省略的情况,这些情况不包括特定情况。

那么是什么使这种特殊情况不同呢?它们的基本区别在于,在原始对象和复制对象之间存在一个函数调用,而函数调用意味着需要考虑额外的约束,特别是调用约定。

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

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

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

对于您的特定问题,如果您使用宏,则代码是内联的,对对象没有限制,并且可以省略副本。但是,如果您添加了一个函数,则不能从返回语句的参数中省略该副本。所以要避开它。不要使用将移动对象的模板,而是创建一个将构造对象的模板:

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

这个拷贝可以被编译器省略。

请注意,我没有处理移动建设,因为你似乎关心甚至移动建设的成本,即使我相信你是在错误的树吠叫。给出一个激励的真实的用例,我很确定这里的人们会想出一些有效的想法。

12.8/31

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

…但是b2的构造需要移动。

不,没有。编译器被允许省略移动;这种情况是否发生取决于具体的实现,取决于几个因素。它也可以移动,但不能复制(在这种情况下必须使用移动而不是复制)。

确实不能保证移动将会被省略。如果必须保证不会发生移动,那么要么使用宏,要么研究实现的选项来控制这种行为,特别是函数内联。

不能将A对象从make_b的参数中复制/移动到已创建的B对象的成员中。

然而,这就是移动语义的全部意义——通过为A提供轻量级的移动操作,您可以避免潜在的昂贵复制。例如,如果A实际上是std::vector<int>,那么可以通过使用move构造函数来避免复制向量的内容,而只是转移内务指针。

这不是什么大问题。它所需要的只是稍微改变一下代码的结构。

代替:

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的特性来做这些