小编典典

什么是复制省略和返回值优化?

all

什么是复制省略?什么是(命名的)返回值优化?它们意味着什么?


阅读 116

收藏
2022-03-15

共1个答案

小编典典

介绍

对于技术概述
复制省略在标准中定义:

12.8 复制和移动类对象[class.copy]

作为

\31) 当满足某些条件时,允许实现省略类对象的复制/移动构造,即使对象的复制/移动构造函数和/或析构函数有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象本应被删除的较晚时间。没有优化就被破坏了。123这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以结合起来消除多个副本):

— 在具有类返回类型的函数的 return 语句中,当表达式是具有与函数返回类型相同的 cvunqualified 类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作

— 在 throw 表达式中,当操作数是非易失性自动对象(函数或 catch 子句参数除外)的名称时,其范围不超出最里面的封闭 try 块的末尾(如果有一)、从操作数到异常对象(15.1)的复制/移动操作可以通过将自动对象直接构造到异常对象中来省略

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

— 当异常处理程序的异常声明(第 15 条)声明与异常对象(15.1)具有相同类型的对象(cv 限定除外)时,可以通过处理异常声明来省略复制/移动操作作为异常对象的别名,如果程序的含义将保持不变,除了为异常声明声明的对象执行构造函数和析构函数。

\123) 因为只有一个对象被销毁而不是两个,并且没有执行一个复制/移动构造函数,所以每构造一个对象,仍然有一个对象被销毁。

给出的例子是:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

并解释说:

这里可以结合省略的标准来消除对类的复制构造函数的两次调用Thing:将本地自动对象复制t到临时对象中以获取函数的返回值,f() 以及将临时对象复制到对象t2中。实际上,本地对象的构造t 可以看作是直接初始化全局对象t2,而该对象的销毁将在程序退出时发生。将移动构造函数添加到 Thing 具有相同的效果,但它是从临时对象到的移动构造t2被省略了。

对于发生复制省略的常见情况
(命名)返回值优化是复制省略的一种常见形式。它指的是从方法中通过值返回的对象的副本被删除的情况。标准中提出的示例说明了命名返回值优化,因为对象是命名的。

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

返回临时值时会发生常规返回值优化:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

发生复制省略的其他常见位置是通过值传递临时值时:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

foo(Thing());

或者当异常被抛出并被 value 捕获时

struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

复制省略的常见限制是:

  • 多个返回点
  • 条件初始化

大多数商业级编译器支持复制省略和 (N)RVO(取决于优化设置)。

复制省略是大多数编译器实现的一种优化,用于在某些情况下防止额外的(可能是昂贵的)复制。它使按值返回或按值传递在实践中可行(有限制)。

这是省略(哈!)as-if 规则的唯一优化形式 - 即使复制/移动对象有副作用,也可以应用复制省略

以下示例取自维基百科

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}

根据编译器和设置,以下输出 都是有效 的:

你好世界!
制作了一份副本。
制作了一份副本。


你好世界!
制作了一份副本。


你好世界!

这也意味着可以创建更少的对象,因此您也不能依赖于调用特定数量的析构函数。您不应该在复制/移动构造函数或析构函数中包含关键逻辑,因为您不能依赖它们被调用。

如果省略了对复制或移动构造函数的调用,则该构造函数必须仍然存在并且必须是可访问的。这确保复制省略不允许复制通常不可复制的对象,例如因为它们具有私有或已删除的复制/移动构造函数。

**C17** :从 C17 开始,直接返回对象时保证复制省略:

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C(); //Definitely performs copy elision
}
C g() {
    C c;
    return c; //Maybe performs copy elision
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f(); //Copy constructor isn't called
}
2022-03-15