C++ 临时对象
临时对象对开发人员来说,可能是个意外情况,因为是编译器偷偷产生的,并不在源码中出现。由于临时对象可能影响程序性能,本文主要探讨这种额外产生的临时对象。
产生临时对象
假设有理数类Rational:
// 有理数
class Rational
{
friend Rational operator+(const Rational&, const Rational&);
public:
Rational(int a= 0, int b = 1)
: m(a), n(b)
{}
private:
int m; // 被除数
int n; // 除数
};
通常,有下面3种等价方式实例化Rational对象,
Rational r1(100); // 1
Rational r2 = Rational(100); // 2
Rational r3 = 100; // 3
除了r1/r2/r3,这三种形式中,只有情形1才能保证不管编译器如何实现,都不会产生临时对象。
对于情形2,可能会产生临时对象Rational(100, 1);
对于情形3,可能会产生临时对象Rational(100, 1)。
产生临时对象后,再调用operator=,拷贝构造r2/r3。
创建r3对象伪码:
{
Rational r3;
Rational _temp;
_temp.Rational::Rational(100, 1); // 构造函数
r3.Rational::Rational(_temp); // 拷贝构造函数
_temp.Rational::~Rational(); // 析构_temp对象
}
类型不匹配
上面例子,情形2、3,是常见的类型不匹配的情况。当用一个整数初始化Rational对象时,编译器会将该整数类型转换为所需类型的对象。这个过程中,可能会产生临时对象。
如下所示:
Rational r;
r = 100;
// <=>
r = Rational(100, 1);
因为Rational没有定义operator=,编译器合成的缺省operator=希望右边是一个Rational对象,因此会将整型数转换为Rational对象。
如何消除临时对象?
- 方法一:使用explicit
C++ 11标准能让程序员禁止编译器进行这种转换,方法是将构造函数声明为explicit。
class Rational
{
public:
explicit Rational(int a= 0, int b = 1)
: m(a), n(b)
{}
...
};
explicit关键字是告诉编译器,反对将修饰的构造函数用作转换构造函数。
- 方法二:重载Rational::operator=
使该函数接受一个整型参数,而不必调用缺省的operator=,期待等式右边是一个Rational对象。
class Rational
{
public:
...
Rational& operator=(int a)
{
m = a;
n = 1;
return *this;
}
...
};
消除循环体中的临时对象
在下面for循环体中,a、b是复数类对象,ib也是个复数,而每次 ib + 1.0得到的也是一个复数。由于类型不匹配,每次循环都会反复创建一个临时对象,将1.0转换为Complex对象。
// 复数类 z = a + bi
class Complex
{
friend Complex operator*(int , const Complex&);
friend Complex operator*(const Complex&, int );
public:
Complex(int a = 0, int b = 0) : a(a), b(b) {}
...
private:
int a;
int b;
};
Complex a, b;
for (int i = 0; i < 100; i++) {
a = i * b + 1.0;
}
既然每次都创建的临时对象对应值不变,那么有没有什么办法避免多次创建临时对象呢?
答案是有的,可以将临时对象的创建前移到循环体前,将临时对象转换成命名Complex对象。
Complex one(1.0);
for (int i = 0; i < 100; i++) {
a = i * b + one;
}
按值传递
传参
函数调用中,经常使用按值传递参数:
// 函数定义
void g(T formalArg)
{
...
}
// 函数调用
T t;
g(t);
这种按值传递等价于:
// T参数类型
// formalArg 形参
// actualArg 实参
T formalArg = actualArg;
调用g()时, 会在栈中创建一个临时对象,将其内容复制给形参formArg。这也是一个临时对象的拷贝构造过程。
如果创建、清除临时对象的代价昂贵,就需要将按值传递改为按引用或地址传递,从而消除临时对象。
传返回值
按值传递也存在于回传返回值中。这种情况,就是前面一篇文章(C++ 返回值优化RVO)中提到的RVO。可以通过多种方式,消除临时对象,比如使用匿名临时对象,或者用std::move将左值转换为右值,这样在调用处就会调用移动构造函数,而不是拷贝构造函数,从而减少一次对象复制。
string operator+(const string &s, const string &p)
{
char *buffer = new char[s.length() + p.length() + 1];
strcpy(buffer, s.c_str()); // Copy first character string
strcat(buffer, p.c_str()); // Add second character string
string result(buffer); // Create return object
delete buffer;
return result; // 返回值是一个临时对象
}
用operator=()消除临时对象
前面主要从类型不匹配或者传值角度出发,避免类型转换或传值,从而消除临时对象。还有另一种方式,就是使用自定义operator=()来消除临时对象。
遇到下面这种情况:
string s1, s2, s3;
// ...
s3 = s1 + s2; // 产生临时对象string(s1+s2)
可以用string::operator+=()代替+,重新代码,以阻止临时对象的创建。
s3 = s1; // operator=() 没有临时对象产生
s3 += s2; // operator+=() 没有临时对象产生
下面代码中,第一段更优雅,但中性能要求苛刻的地方,第二段会更有效,不会产生临时对象。
// 第一段 会产生3个临时对象
s5 = s1 + s2 + s3 + s4;
// 第二段 不会产生临时对象
s5 = s1;
s5 += s2;
s5 += s3;
s5 += s4;
总结
- 临时对象会以构造函数和析构函数的形式损失性能;
- 将构造函数声明为explicit,可以阻止编译器中偷偷进行类型转换;
- 编译器为了解决类型不匹配问题,创建临时对象,此时可以通过函数重载避免;
- 应尽量使用引用传递参数和对象,避免对象复制;
- 操作符是+-*/时,可使用operator=消除临时对象。
参考
[1]DovBulka, DavidMayhew, 布尔卡,等. 提高C++性能的编程技术[M]. 清华大学出版社, 2003.