C++中的临时对象

Temporary object,临时对象。一听名字就明白,这个对象的意义不大,只是临时中转一下或者存在一下,有的可能连个存在感都刷不到就消失了。但不要小看这种临时对象,对C/C++这种以效率严苛为前提的编程环境下,它就是效率低下的某种代名词。

临时对象

Temporary object,临时对象。一听名字就明白,这个对象的意义不大,只是临时中转一下或者存在一下,有的可能连个存在感都刷不到就消失了。但不要小看这种临时对象,对C/C++这种以效率严苛为前提的编程环境下,它就是效率低下的某种代名词。 但是,临时对象又无法完全避免,所以,怎么控制并减少临时对象的产生就是一个技术活儿了。

产生的时机

那么,在什么情况下会产生临时对象呢?在不同的编译器和不同的标准下,可能都有所不同,下面就分析一下产生临时对象的具体场景:

参数传递

一般值传递对象都会产生临时的对象:

#include <iostream>

class Teacher {
public:
  Teacher() { std::cout << "call Teacher construct func,old_ default is:" << old_ << std::endl; }
  Teacher(int old) : old_(old) { std::cout << "call Teacher construct func,set old_ is:" << old_ << std::endl; }
  Teacher(const Teacher &t) { std::cout << "call Teacher copy construct func" << std::endl; }
  ~Teacher() { std::cout << "call Teacher deconstruct func!" << std::endl; }

public:
  int old_ = 10;
};
void TestPars(Teacher t) {
  std::cout << "Teacher old is:" << t.old_ << std::endl;
}

int main() {

  TestPars(11);
  return 0;
}

注意,编译时如果版本太高需要进行限制,比如此工程的运行环境是C++17,需要使用禁用优化并限定版本编译:

g++ -std=c++11 -fno-elide-constructors -g -o test main.cpp

运行结果:

call Teacher construct func,set old_ is:11
call Teacher copy construct func
Teacher old is:10
call Teacher deconstruct func!
call Teacher deconstruct func!

返回值

产生临时变量最容易就是在返回对象时:

#include <iostream>

class Teacher {
public:
  Teacher() { std::cout << "call Teacher construct func,old_ default is:" << old_ << std::endl; }
  Teacher(int old) : old_(old) { std::cout << "call Teacher construct func,set old_ is:" << old_ << std::endl; }
  Teacher(const Teacher &t) { std::cout << "call Teacher copy construct func" << std::endl; }
  ~Teacher() { std::cout << "call Teacher deconstruct func!" << std::endl; }

public:
  int old_ = 10;
};

Teacher GetTeacher() {
  Teacher t;
  return t;
}
int main() {
  //Teacher t1;
  //GetTeacher() = t1;//OK,可以测试并想一下
    Teacher t = GetTeacher();//注意:此处t不声明照样产生临时变量
    return 0;

}

运行结果:

//注意:使用C++17编译器则始终是两个析构函数

tempObject$ g++ -std=c++11 -fno-elide-constructors -g -o test main.cpp
tempObject$ ./test
call Teacher construct func,old_ default is:10
call Teacher deconstruct func!
call Teacher deconstruct func!
call Teacher deconstruct func!

###类型转换

不同类型之间的隐式转换一般也可能产生临时对象 :

int main() {
  //Teacher tmp;
  //tmp = 88;
  Teacher tmp = 88;
  return 0;
}

运行结果:

call Teacher construct func,set old_ is:88
call Teacher copy construct func
call Teacher deconstruct func!
call Teacher deconstruct func!

这种情况和环境有比较大的关系,一般推荐是先声明对象变量,然后再用这个变量设置值,可在实际采用非优化的情况下,二者一致。

中间计算结果

#include <iostream>

class Teacher {
public:
  Teacher() { std::cout << "call Teacher construct func,old_ default is:" << old_ << std::endl; }
  Teacher(int old) : old_(old) { std::cout << "call Teacher construct func,set old_ is:" << old_ << std::endl; }
  Teacher(const Teacher &t) { std::cout << "call Teacher copy construct func" << std::endl; }
  Teacher operator+(const Teacher &t1) {
    Teacher t;
    t.old_ = old_ + t1.old_;

    return t;
    // return Teacher(old_ + t1.old_);
  }
  ~Teacher() { std::cout << "call Teacher deconstruct func!" << std::endl; }

public:
  int old_ = 10;
};

void GetSum() {
  Teacher t1, t2, t3;
  Teacher t = t1 + t2 + t3;
  std::cout << "sum is:" << t.old_ << std::endl;
}

int main() {

  GetSum();
  std::cout << "cacl temp create!" << std::endl;
  return 0;
}

运行结果:

call Teacher construct func,old_ default is:10
call Teacher construct func,old_ default is:10
call Teacher construct func,old_ default is:10
call Teacher construct func,old_ default is:10
call Teacher construct func,old_ default is:10
call Teacher deconstruct func!
sum is:30
call Teacher deconstruct func!
call Teacher deconstruct func!
call Teacher deconstruct func!
call Teacher deconstruct func!
cacl temp create!

函数const引用参数

这个也是隐式转换:

void TestConstTmp(const std::string &s) { std::cout << "tmp value:" << s << std::endl; }
int main() {
  char buf[] = "this is test!";
  TestConstTmp(buf);
  return 0;
}

//或者

void TestConstTmp(const Teacher &t) { std::cout << "tmp value:" << t.old_ << std::endl; }

int main() {
  TestConstTmp1(88);
  return 0;
}

不过这种在实际模拟中未能模拟出临时对象的现象,所以待验证。而且需要注意的是,必须是const &方可。

优化

优化的最基本方式

在上面提到过,优化最简单的方法便是使用更高标准的编译器,或者开启更高级别的优化选项。

对于类型转换产生临时对象的优化可以声明显示构造和重载operator=来解决

对于返回值只要按RVO和NRVO(C++11)的约定即可优化

对于传参产生的临时对象,可以采用引用传参来优化

对于中间值计算及其它,可以在代码层面避免,比如不采用多于两上的连加,不使用隐式类型转换等等。

在STL库容器增加元素增加了emplace系列,用它来替换insert,push等,减少临时对象的产生。这也是一种标准进步后的库的进步。

临时对象销毁

主要分两种情况,一种是做为引用的临时对象,其在绑定对象销毁时销毁;其它的非上述情况,则在作用域范围结束后销毁(指表达式的语句或者if,while等控制表达式的末尾)。

总结

如果工程对效率要求不高,其实对临时对象的处理也不用费什么心思。赶哪的集用哪的斗,不用刻意非得怎么着才行。主打一个兼顾效率和开发的平衡,优化未必时时都是一个必选项。