程序转换语义(Program Transformation Semantics)
看到以下代码,你会想到哪些?也许你会有以下两个推断:
- 每次调用GetNode时,都会以值的方式返回一个Node对象。
- 每次调用GetNode时,都会调用Node类的拷贝构造函数。
class Node;
Node GetNode()
{
Node n;
return n;
}
以上两个推断看起来合情合理,但通过上一节我们知道,Node是否具有拷贝构造函数,取决于其自身的定义,而且编译器有可能在背后偷偷优化你的代码,所以以上两个推断是否正确,一方面要看类Node的定义;另一方面也要看编译器有没有做优化。
显式的初始化(Explicit initialization)
现在有一个对象的定义,Node n。然后有3个基于对象n的显式初始化如下,
void func()
{
Node n1(n);
Node n2 = n;
Node n3 = Node(n);
}
那么这个代码片段所需要的程序转换就有两部分:1. 以非初始化的方式重写每个定义;2. 插入对拷贝构造函数的调用。转换后程序的伪代码如下:
Node n;
void func()
{
Node n1;
Node n2;
Node n3;
n1.Node::Node(n);
n2.Node::Node(n);
n3.Node::Node(n);
}
参数初始化(Argument initialization)
C++的标准中指出,当一个类对象以传值的形式作为函数的参数时,其等同于以下的形式:
Node n = args;
其中n是形参,args是实参。那么对于以下函数:
void func(Node n);
经转换之后,其伪代码如下:
Node n;
Node _temp_node;
_temp_node.Node::Node(n);
func(_temp_node); // void func(Node &);
此时应该注意,func的参数已经由传值改变为传引用。
返回值的初始化(Return value initialization)
对于函数func,其定义为:
Node func()
{
Node n;
return n;
}
那么函数的返回值是如何从局部对象n拷贝而来的呢?在cfront中,这一过函数返回值的初始化过程可以分为两步,1. 为该函数增加一个引用类型的参数,该参数持有该函数的返回值; 2. 调用该类的拷贝构造函数以初始化该参数。经过转换之后,该函数的伪代码如下:
Node _ret_node;
void func(Node& _ret_node)
{
Node n;
_ret_node.Node::Node(n);
return ;
}
编译器级别的优化(Optimization at the compiler level)
我们上面看到的这种优化函数返回值的方式被称为具名返回值(Named Return Value, NRV)优化。这种优化方式在C++中是一种强制优化措施(虽然标准并没有这样要求)。这种优化方式相对于直接返回值然后进行bitwise copy的效率会更高一些。但这种优化要求该类必需具有拷贝构造函数。
拷贝构造函数有时候也会带来一些性能开销。例如,对于以下初始化方式:
Node n(1);
Node n1 = Node(1);
Node n2 = (Node)1;
对于第一种定义,n是通过构造函数直接初始化,而n1和n2的初始化方式则会产生一个临时对象,并且通过调用拷贝构造函数之后,还需要调用该临时对象的析构函数。这里就多个一个析构的开销。但是否应该取消拷贝构造函数呢?在作者写这本书时,标准委员会还没有定论,因为拷贝构造函数显然太重要了,不能轻易取消。
拷贝构造函数:要还是不要(Copy constructor: to have or not have)
对于下面这个类来说,它是否需要拷贝构造函数?
class Point3d
{
public:
Point3d(float x, float y, float z) : m_x(x), m_y(y), m_z(z) {}
float m_x;
float m_y;
float m_z;
};
对类Point3d来说,其没有显式定义的拷贝构造函数,也不满足编译器合成拷贝构造函数的四个条件之一,所以其没有拷贝构造函数。虽然没有拷贝构造函数,但是按位拷贝能够很好的满足程序的需求,按位拷贝不会造成内存的泄漏或者内存的重叠。所以没有拷贝构造函数的话,也没有性能或者安全性上的损失。
那么该类真的就不需要拷贝构造函数了吗?如果该类对象需要大量的初始化操作,尤其是作为函数的返回值时,此时就应该考虑给该类声明一个拷贝构造函数。声明拷贝构造函数之后,编译器就能够进行NRV优化了。
总结
拷贝构造函数能够在一定程度上优化程序的效率,尤其是NRV优化。要利用NRV优化,需要判断一个类是否有拷贝构造函数(编译器合成的或者显式声明的)。