每周技巧 #5:消失术

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #5: Disappearing Act

原文最初作为 TotW #5 发布于 2012 年 6 月 26 日。

更新于 2020 年 6 月 1 日。

“直到失去才知道自己曾拥有什么。”– Cinderella

有时候,为了正确使用一个 C++ 库,你既需要理解这个库,也需要理解这门语言。那么,下面这段代码有什么问题?

1
2
3
4
5
// 不要这样做
std::string s1, s2;
...
const char* p1 = (s1 + s2).c_str();             // 避免!
const char* p2 = absl::StrCat(s1, s2).c_str();  // 避免!

(s1+s2)absl::StrCat(s1,s2) 都会创建临时对象。在这两个例子中,临时对象都是字符串;同样的规则也适用于任何对象。成员函数 c_str() 返回一个指针,指向的数据只在这个临时对象存活期间有效。那么临时对象会活多久?按照 C++17 标准 [class.temporary] 的说法:“临时对象会在求值包含其创建点的完整表达式时,作为最后一步被销毁。”(“完整表达式”指“不是另一个表达式的子表达式的表达式”。)在上面的每个例子中,一旦赋值运算符右侧的表达式完成,临时值就会被销毁,c_str() 的返回值也就变成了悬垂指针。简而言之:当你遇到分号时,临时对象通常已经成为历史,有时甚至更早。哎呀!该如何避免这类问题呢?

方案 1:在完整表达式结束前用完临时对象

1
2
3
// 安全(虽然例子有点傻):
size_t len1 = strlen((s1 + s2).c_str());
size_t len2 = strlen(absl::StrCat(s1, s2).c_str());

方案 2:保存临时对象

反正你已经在创建一个对象(在栈上),为什么不把它保留一会儿呢?这比乍看起来更便宜。由于有一种叫“返回值优化”的机制(许多值类型还有移动语义,见 技巧 #77),临时对象会直接构造在你“赋值”给的变量中,而不会被复制:

1
2
3
4
// 安全(而且比你想象的更高效):
std::string tmp_1 = s1 + s2;
std::string tmp_2 = absl::StrCat(s1, s2);
// tmp_1.c_str() 和 tmp_2.c_str() 都是安全的。

方案 3:保存到临时对象的引用

C++17 标准 [class.temporary]:“引用所绑定的临时对象,或者引用所绑定的子对象所属的完整对象,会在该引用的生命周期内持续存在。”

由于返回值优化,这通常并不比直接保存对象本身(方案 2)更便宜,而且它可能让人困惑或担心(见 技巧 #101)。(确实需要使用生命周期延长的例外情况应该写注释说明!)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 同样安全:
const std::string& tmp_1 = s1 + s2;
const std::string& tmp_2 = absl::StrCat(s1, s2);
// tmp_1.c_str() 和 tmp_2.c_str() 都是安全的。
// 下面的行为非常微妙,使用时要小心:
// 如果编译器能看到你正在保存一个指向
// 临时对象内部内容的引用,它会让整个
// 临时对象继续存活。
// struct Person { string name; ... }
// GeneratePerson() 返回一个对象;GeneratePerson().name
// 显然是一个子对象:
const std::string& person_name = GeneratePerson().name; // 安全
// 如果编译器无法判断,你就有风险。
// class DiceSeries_DiceRoll { `const string&` nickname() ... }
// GenerateDiceRoll() 返回一个对象;编译器无法判断
// GenerateDiceRoll().nickname() 是否是子对象。
// 下面这行可能保存悬垂引用:
const std::string& nickname = GenerateDiceRoll().nickname(); // 糟糕!

方案 4:把函数设计成不返回对象???

许多函数遵循这个原则,但也有很多函数并不这样做。有时候,返回一个对象确实比要求调用方传入输出参数指针更好。你需要意识到什么时候可能创建临时对象。任何返回指向对象内部内容的指针或引用的东西,在作用于临时对象时都可能有问题。c_str() 是最明显的罪魁祸首,但 protobuf 的 getter(无论是否 mutable)以及一般意义上的 getter,都可能同样有问题。

每周技巧 #3:字符串拼接:`operator+` 与 `StrCat()`

上一节

每周技巧 #10:拆分字符串,而不是吹毛求疵

下一节