繁体   English   中英

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

[英]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() );
}

如果有人知道如何更巧妙地实现这一点,我会很感兴趣地看到它。

之前的讨论:

这在某种程度上源于对以下问题的回答:

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

正如 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 和参数放在同一位置,因此$tmp1x不能是 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.

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