临时对象

C++中有这样一种对象:它在代码中看不到,但是确实存在。它就是临时对象—由编译器定义的一个没有命名的非堆对象(non-heap object)。

临时对象通常产生于以下4种情况:

  1. 类型装换
  2. 按值传递
  3. 按值返回
  4. 对象定义

1、类型转换

它通常是为了让函数调用成功而产生临时对象。发生于 “传递某对象给一个函数,而其类型与它即将绑定上去的参数类型不同” 的时候。

例如:

void test(const string& str);    
char buffer[] = "buffer";  
test(buffer); // 此时发生类型转换  

此时,编译器会帮你进行类型转换:它产生一个类型为string的临时对象,该对象以buffer为参数调用string constructor。当test函数返回时,此临时对象会被自动销毁。

注意:对于引用(reference)参数而言,只有当对象被传递给一个reference-to-const参数时,转换才发生。如果对象传递给一个reference-to-non-const对象,不会发生转换。

例如:

void upper(string& str);    
char lower[] = "lower";   
upper(lower); // 此时不能转换,编译出错  

此时如果编译器对reference-to-non-const对象进行了类型转换,那么将会允许临时对象的值被修改。而这和程序员的期望是不一致的。

试想,在上面的代码中,如果编译器允许upper运行,将lower中的值转换为大写,但是这是对临时对象而言的,char lower[]的值还是“lower”,这和你的期望一致吗?

有时候,这种隐式类型转换不是我们期望的,那么我们可以通过声明constructor为explicit来实现。explicit告诉编译器,我们反对将constructor用于类型转换。

例如:

explicit string(const char*);  

2、按值传递

这通常也是为了让函数调用成功而产生临时对象。当按值传递对象时,实参对形参的初始化与

T formalArg = actualArg 的形式等价。

例如:

1
2
3
void testT formalArg);    
T actualArg;  
test(actualArg);  

此时编译器产生的伪码为:

1
2
3
4
T _temp;  
_temp.T::T(acutalArg); // 通过拷贝构造函数生成_temp  
g(_temp);  // 按引用传递_temp  
_temp.T::~T(); // 析构_temp  

因为存在局部参数formalArg,test()的调用栈中将存在formalArg的占位符。编译器必须复制对象actualArg的内容到formalArg的占位符中。所以,此时编译器生成了临时对象。

3、按值返回

如果函数是按值返回的,那么编译器很可能为之产生临时对象。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Integer {  
public:  
  friend Integer operator+(const Integer& a, const Integer& b);  
  Integer(int val=0): value(val) {  
  }  
  Integer(const Integer& rhs): value(rhs.value) {  
  }  
  Integer& operator=(const Integer& rhs);  

  ~Integer() {  
  }          
private:  
  int value;    
};  

Integer operator+(const Integer& a, const Integer& b) {  
  Integer retVal;  

  retVal.value = a.value + b.value;  

  return retVal;  
}  

Integer c1, c2, c3;  
c3 = c1 + c2;  

编译器生成的伪代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Integer _tempResult; // 表示占位符,不调用构造函数  
operator+(_tempResult, c1, c2); // 所有参数按引用传递  
c3 = _tempResult; // operator=函数执行  

Integer operator+(const Integer& _tempResult, const Integer& a, const Integer& b) {  
  struct Integer retVal;  
  retVal.Integer::Integer(); // Integer(int val=0)执行        
  retVal.value = a.value + b.value;  

  _tempResult.Integer::Integer(retVal); // 拷贝构造函数Integer(const Integer& rhs)执行,生成临时对象。    
  retVal.Integer::~Integer(); // 析构函数执行    
  return;  
}     
  return retVal;  
}  

如果对operator+进行返回值优化(RVO:Return Value Optimization),那么临时对象将不会产生。

例如:

1
2
3
Integer operator+(const Integer& a, const Integer& b) {    
  return Integer(a.value + b.value);  
}  

编译器生成的伪代码:

1
2
3
4
5
Integer operator+(const Integer& _tempResult, const Integer& a, const Integer& b) {  
  _tempResult.Integer::Integer(); // Integer(int val=0)执行  
  _tempResult.value = a.value + b.value;    
  return;  
}  

对照上面的版本,我们可以看出临时对象retVal消除了。

4、对象定义

例如:

1
2
3
Integer i1(100); // 编译器肯定不会生成临时对象  
Integer i2 = Integer(100); // 编译器可能生成临时对象  
Integer i3 = 100; // 编译器可能生成临时对象 

然而,实际上大多数的编译器都会通过优化省去临时对象,所以这里的初始化形式基本上在效率上都是相同的。

临时对象的生命期

有关临时对象的生命周期有三种情况:

1.一般情况 临时性对象的被摧毁,应该是对完整表达式(full-expression)求值过程中的最后一个步骤。该完整表达式造成临时对象的产生。

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
using namespace std;

class A
{
public:
A(int i): m_i(i)
{
cout << "A(): "<< m_i << endl;
}

   ~A()
{
cout << "~A(): "<< m_i << endl;
}

   A operator+(const A& rhs)
{
cout << "Aoperator+(const A& rhs)" << endl;
return A(m_i + rhs.m_i);
}

   int m_i;
};

int main()
{
A a1(1), a2(2);
a1 + a2;
cout <<"------------------------------------" << endl; //运行到这里,a1 + a2产生的临时变量已经被释放
return 0;
}

运行结果:

特例1:含有表达式执行结果的临时性对象

凡含有表达式执行结果的临时性对象,应该存留到object的初始化操作完成为止。

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<iostream>
using namespace std;

class A
{
public:
A(int i = 0): m_i(i)
{
cout << "A(): "<< m_i << endl;
}

   ~A()
{
cout << "~A(): "<< m_i << endl;
}

   A operator+(const A& rhs)
{
cout << "Aoperator+(const A& rhs)" << endl;
return A(m_i + rhs.m_i);
}

   A& operator=(const A& rhs)
{
cout << "A&operator=(const A& rhs)" << endl;
m_i += rhs.m_i;
return *this;
}

   int m_i;
};

int main()
{
A a1(1), a2(2);
A a3;
a3 = a1 + a2; //a1 + a2产生的临时变量在a3的赋值操作完成后,才释放
return 0;
}

运行结果:

特例2. 临时性对象被绑定于一个reference

如果一个临时性对象被绑定于一个reference,对象将残留,直到被初始化之reference的生命结束,或直到临时对象的生命范畴(scope)结束——视哪一种情况先到达而定。

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<iostream>
using namespace std;

class A
{
friend ostream& operator<<(ostream& os, const A&);
public:
A()
{

   }

   A(const A&)
{
cout << "A(constA&)" << endl;
}

   ~A()
{
cout << "~A()"<< endl;
}
};

ostream& operator<<(ostream&os, const A&)
{
os << "ostream& operator<<(ostream&os, const A&)" << endl;
return os;
}

const A&f(const A& a)
{
return a;
}

int main(intargc, char* argv[])
{
{
const A& a = A();
cout << "-------------------"<< endl;
}//直到被初始化之reference的生命结束

cout  << f(A()) << endl; //直到临时对象的生命范畴(scope)结束:
//临时对象的const引用在f的参数上(而不是返回值)。
//这个引用在f()返回的时候就结束了,但是临时对象未必销毁。
cout << "-------------------" << endl;

return 0;
}

运行结果:

具名返回值优化(Named Return Value)

NRV是Named Return Value的简称。NRV优化简单的说:有一条语句,A a = f();其中f()是一个函数,函数里边申请了一个A的对象b,然后把对象b返回。在对象返回的时候,一般情况下要调用拷贝函数,把函数f()里边的局部对象b拷贝到函数外部的对象a。但是如果用了NRV优化,那就不必要调用拷贝构造函数,编译器可以这样做,把a的地址传递进函数f(),然后不让f()申请要返回的对象b的空间,用a的地址来代替b的地址,这样当要返回对象b的时候,就不必要拷贝了,因为b就是a,省去了局部变量b,省去了拷贝的过程。

函数返回局部对象的拷贝的一般实现方式

1
2
3
4
5
6
7
class X;  
X bar()  
{  
    X x1;  
    // 处理 x1..  
    return x1;  
}

针对”Xbar()”这样的函数,是返回class X的一个对象的拷贝。其返回值是一个对象,比如叫做x2。在执行return时,x2通过调用拷贝构造函数,拷贝对象x1来实现其初始化。也就是说,这里会存在两个对象x1、x2。那么这种返回对象的拷贝,是怎么实现的呢?一般来说,C++编译器会将上段代码中bar的实现转换成如下的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void bar(X& __result)   // 加上一个额外参数  
{  
    // 预留x1的内存空间  
    X x1;  
    // 编译器产生的默认构造函数的调用,  
    x1.X::X();  
    // 处理 x1..  
    // 编译器产生的拷贝操作  
    __result.X::X(x1);        
    return;  
}  

    X x2;                 //这里只是预留内存,执行默认构造函数,并未调用初始化函数  
    bar(x2);  

通过上述代码,我们可见编译器对于返回对象拷贝的处理方式。

1、函数添加一个额外参数,为返回对象的引用;

2、函数调用前,先申请欲返回对象x2的内存空间;

3、将对象x2的引用传入函数中,并在函数返回前,调用x2的拷贝构造函数。

通过上述实现方式

X x2 = bar();  

被转换成了

X x2;

bar(x2);  

NRV(Named Return Value)优化

上面的实现中,存在着x1、x2两个对象,而x1的生命周期转瞬即逝。而且对于bar()的调用者来说,根本就没有x1这个对象,调用者想要的只有x2。这样的实现能不能够将其优化变得更快呢。编译器有一种优化方式,直接将x2替代x1。编译器转换后的伪代码如下。

1
2
3
4
5
6
7
void bar(X& __result)  
{  
    // 调用__result的默认构造函数  
    __result.X::X();  
    // 处理__result  
    return;  
}  

从代码看出,这里只有一个对象,也就是传入的x2。NRV优化后的实现,比原来的实现省去了如下操作:

  1. 在堆栈中预留x1的内存;

  2. 调用X的默认构造函数,构造x1

  3. 调用X的拷贝构造函数,构造x2

  4. 调用x1的析构函数

  5. 堆栈中回收x1的内存

但是多了一个操作,就是调用X的默认构造函数,构造x2。对于函数的调用者(只关心x2不关心x1)来说,只有一个区别,就是x2的构造方式由调用拷贝构造函数,转变成了调用默认构造函数。

##NRV优化的触发条件 《深度探索C++对象模型》中作者提到程序员必须给class X定义拷贝构造函数才能触发NRV优化,不然还是按照最初的较慢的方式执行。在现在的编译器中NRV优化和拷贝构造函数是否定义没关系。

因为早期的 cfront需要一个开关来决定是否应该对代码实行NRV优化,这就是是否有客户(程序员)显式提供的拷贝构造函数:如果客户没有显示提供拷贝构造函数,那么cfront认为客户对默认的逐位拷贝语义很满意,由于逐位拷贝本身就是很高效的,没必要再对其实施NRV优化;但如果客户显式提供了拷贝构造函数,这说明客户由于某些原因(例如需要深拷贝等)摆脱了高效的逐位拷贝语义,其拷贝动作开销将增大,所以将应对其实施NRV 优化,其结果就是去掉并不必要的拷贝函数调用。”

作者在书中一直都是以cfront来举例说明的,所以其才会有NRV开关的说法。

测试如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CTest  
{  
public:  
CTest()  
{  
    cout << "CTest()" << this << endl;  
}  
CTest(const CTest& rcTest)  
{  
    cout << "CTest(CTest)" << this << endl;  
}  
~CTest()  
{  
    cout << "~CTest()" << this << endl;  
}  
private:  
int a;  
};  

CTest foo()  
{  
CTest oTestInFoo;  
return oTestInFoo;  
}  

int main()  
{  
CTest oTest = foo();  
return 0;  
}  

在不同的编译环境的执行结果分别为:

VS2005(Debug)

VS2005(Release)

g++(-c -o)

也就是说,在vs2005的release环境和g++中,都触发了编译器的NRV优化。

然后,再将代码中的class CTest的拷贝构造函数去掉,执行结果依次为:

VS2005(Debug)

VS2005(Release)

g++(-c -o)

我们去掉class CTest的拷贝构造函数后,按照《深度探索》中所说,class CTest的拷贝动作只需要bitwise copy就可以实现。所以编译器也不会给其合成一个implicit的拷贝构造函数。也就是说,这个时候class CTest是没有拷贝构造的。但执行结果和去掉拷贝构造前一样,vs2005译编的release程序和g++中,均使用了NRV优化。

最后将CTest的代码改为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CSub  
{  
public:  
CSub(){}  
CSub(const CSub& rcSub)  
{  
cout << "CSub(CSub)" << endl;  
}  

};  

class CTest  
{  
public:  
CTest()  
{  
cout << "CTest()" << this << endl;  
}  
~CTest()  
{  
cout << "~CTest()" << this << endl;  
}  
private:  
int a;  
CSub oSub;  
};  

这个时候的执行结果为:

VS2005(Debug)

VS2005(Release)

g++(-c -o)

实验结果显示,不管是类有explicit的构造、implicit的拷贝构造还是没有拷贝构造,在vs2005的Release和g++下都会触发NRV优化,在vs2005(Debug)下都没有NRV优化。所以可以得出结论,在这两个编译器中,NRV优化和拷贝构造函数是否定义没关系。

现在已经确定的是vs(Release)和g++都会执行NRV优化。而NRV优化会导致原本预想中的调用拷贝构造函数变成调用别的构造函数(视函数中的对象调用的构造函数而定)。这一点一定要注意,因为一旦这个时候,拷贝构造函数和别的构造函数提供的功能不同(其实一直都不应该这样),会导致debug和release出现执行结果不同的情况。

参考:

http://www.myexception.cn/cpp/1456175.html

http://blog.csdn.net/zha_1525515/article/details/7170059

http://blog.csdn.net/chdhust/article/details/9617567