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.

posted @ 2023-02-25 16:25  明明1109  阅读(127)  评论(0编辑  收藏  举报