【C++】复制省略(Copy elision)

1. 复制构造函数

1.1 定义

复制构造函数是一种特殊的构造函数,形参是本类对象的引用,作用是使用一个已存在的对象(由复制构造函数参数指定)去初始化同类的一个新对象

语法形式

class 类名
{
public:
	类名(类名 &对象名); //复制构造函数
};
类名::类名(类名 &对象名) //类体外实现复制构造函数
{
	函数体
}

1.2 调用

  1. 用类的一个对象初始化该类另一个对象
  2. 函数的形参是类的对象
  3. 函数的返回值是类的对象

示例

#include <iostream>
#include <iomanip>
using namespace std;
class Test
{
public:
	Test();
	Test(const Test &);
	~Test();
};

Test::Test()
{
	cout << setw(20) << "default constructor " << this << endl;
}

Test::Test(const Test &rhs)
{
	cout << setw(20) << "copy constructor " << this << endl;
}

Test::~Test()
{
	cout << setw(20) << "destructor " << this << endl;
}

Test fun(Test b) //情况2
{
	Test c = b; //情况1
	return c;	//情况3
}

int main()
{
	Test a;
	fun(a);

	return 0;
}

结果VS2022 _MSC_VER == 1931 Debug x64

default constructor 0000005947CFF734
   copy constructor 0000005947CFF814
   copy constructor 0000005947CFF5F4
   copy constructor 0000005947CFF854
         destructor 0000005947CFF5F4
         destructor 0000005947CFF814
         destructor 0000005947CFF854
         destructor 0000005947CFF734

分析

  1. main函数中创建对象a,调用默认构造函数Test()
  2. fun函数的形参为Test类的对象。形实结合时,为形参b分配内存空间并将实参a的值传递给b,调用复制构造函数Test(const Test &)
  3. fun函数中用已存在的对象b初始化同类新对象c,调用复制构造函数Test(const Test &)
  4. fun函数中返回值为Test类的对象。执行return语句时,创建无名临时对象并将c的值传递给临时对象,调用复制构造函数Test(const Test &)
  5. 对象c生存期结束,调用析构函数~Test()
  6. 对象b生存期结束,调用析构函数~Test()
  7. 无名临时对象生存期结束,调用析构函数~Test()
  8. 对象a生存期结束,调用析构函数~Test()

虽然fun函数非常简单,只是将对象传入再传出,但是需要反复调用复制构造及析构函数。是否能够省略冗余的复制操作,提高C++代码运行效率?

2. 复制省略

复制省略是自C++11标准起提出的一项编译优化技术,通过省略复制及移动构造函数的调用,实现零复制的值传递语义。

2.1 强制省略复制操作(自C++17起)

以下情况要求编译器省略类对象的复制和移动构造,即使复制/移动构造函数和析构函数具有可观察的副作用。这些对象将直接构造到它们本来要复制/移动到的内存中。复制/移动构造函数不必存在或可访问。

  1. return语句中,当操作数是与函数返回类型相同的类类型纯右值(忽略 cv 限定)时。

    Test fun()
    {
    	return Test();
    }
    
    int main()
    {
    	fun();
    	return 0;
    }
    

    结果VS2022 _MSC_VER == 1931 Debug x64

    default constructor 0000007F781DF664
             destructor 0000007F781DF664
    
  2. 在对象初始化中,当初始化表达式是与目标对象类型相同的类类型纯右值(忽略 cv 限定)时。

    int main()
    {
    	Test t = Test();
    	return 0;
    }
    

    结果VS2022 _MSC_VER == 1931 Debug x64

    default constructor 0000004EE62FF6E4
             destructor 0000004EE62FF6E4
    

    注:以上规则不再是一项优化技术,而是语言标准。VS2022 Debug和Release版本均执行强制省略复制操作,无法禁用。

    疑问:将复制构造函数声明为非公有后,VS2022 Debug和Release版本均编译失败,而g++编译器(g++ (Rev9, Built by MSYS2 project) 11.2.0)编译成功。Why?强制省略复制不是允许复制/移动构造函数不存在或不可访问吗?

2.2 非强制省略复制操作(自C++11起)

以下情况允许但不要求编译器省略类对象的复制和移动构造,即使复制/移动构造函数和析构函数具有可观察的副作用。这些对象将直接构造到它们本来要复制/移动到的内存中。复制构造函数必须存在且可访问,否则程序是病态的。

  1. 具名返回值优化(named return value optimization, NRVO):在return语句中,当操作数是具有自动存储期的non-volatile 具名对象,不是函数形参或catch子句形参,且具有与函数返回类型相同的类类型(忽略 cv 限定)时。

    Test fun()
    {
    	Test a;
    	return a;
    }
    
    int main()
    {
    	Test t = fun();
    	return 0;
    }
    

    结果VS2022 _MSC_VER == 1931 Debug x64

    default constructor 000000B6CD5EF734
       copy constructor 000000B6CD5EF874
             destructor 000000B6CD5EF734
             destructor 000000B6CD5EF874
    

    结果VS2022 _MSC_VER == 1931 Release x64

    default constructor 0000007E0C91FD28
             destructor 0000007E0C91FD28
    

    注:VS2022 Debug版本默认禁用NRVO优化,Release版本默认执行NRVO优化。

  2. 返回值优化(return value optimization, RVO):在return语句中,当操作数是无名临时对象,且具有与函数返回类型相同的类类型(忽略 cv 限定)时。自C++17起是强制要求的,参考2.1 强制省略复制操作

  3. 在对象初始化中,当源对象是无名临时对象,且与目标对象具有相同类型(忽略 cv 限定)时。自C++17起是强制要求的,参考2.1 强制省略复制操作


版权声明:本文为weixin_55760163原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。