每周技巧 #116:在参数上保留引用
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #116: Keeping References on Arguments。
原文最初作为 TotW #116 发布于 2016 年 5 月 26 日。
更新于 2020 年 6 月 1 日。
“从绘画到图像,从图像到文字,从文字到声音,一种想象中的指针指示、展示、固定、定位、强加一套引用系统,并试图稳定一个唯一空间。”— Michel Foucault, This is Not a Pipe
const 引用与指向 const 的指针
作为函数参数使用时,const 引用相较于指向 const 的指针有几个优点:它们不能为 null,而且很清楚函数没有取得对象所有权。但它们也有一些其他差异,有时会带来问题:它们更隐式(也就是说,调用点没有任何东西显示我们正在取得引用),并且它们可以绑定到临时对象。
类中的悬垂引用风险
以下面这个类为例:
|
|
它看起来很合理。但如果我们用字符串字面量构造一个 Foo 会怎样?
|
|
创建 foo 时,content_ 成员会绑定到一个临时 std::string 对象。这个临时对象由字面量创建,并传给构造函数。临时字符串会在它被创建的那一行结束时离开作用域。现在 foo.content_ 是对一个已经不存在对象的引用。访问它是未定义行为,什么都可能发生:从测试里看起来正常,到生产中严重出错。
一个解决方案:使用指针
在我们的例子中,最简单的解决方案可能是按值传递并存储字符串。但假设我们确实需要引用原始参数,例如它不是字符串,而是某种更有意思的类型。解决方案是按指针传递参数:
|
|
现在下面的代码会直接无法编译:
|
|
而在调用点也会很清楚,这个对象可能保留参数地址:
|
|
再进一步,少一条注释:存储引用
你可能注意到,我们说了两次指针不能为 null 且不拥有对象:一次在构造函数文档里,一次在实例变量注释里。这有必要吗?考虑下面这样:
|
|
引用类型成员的一个缺点是你不能重新赋值,这意味着你的类不会有拷贝赋值运算符(拷贝构造函数仍然可以)。但为了遵守 rule of 3,显式删除它可能是合理的。如果你的类应该可赋值,你就需要非 const 指针,仍然可以指向 const 对象。技巧 #177 会更详细讨论这一点。
如果你想纵深防御,认为某个调用方可能意外传入 null 指针,可以使用 *ABSL_DIE_IF_NULL(arg1) 来触发崩溃。注意,直接解引用 null 指针并不像通常认为的那样保证崩溃;它是未定义行为,不应该依赖。这里可能发生的是:由于引用实现为指针,它只是被复制,真正访问该字段时才会在之后崩溃。
结论
如果参数会被复制,或者只在构造函数中使用、不会在构造出的对象中保留引用,那么把参数按 const 引用传给构造函数仍然没问题。在其他情况下,请考虑按指针传参(指向 const 或非 const 都可以)。还要记住,如果你实际上正在转移对象所有权,就应该用 std::unique_ptr 传递。
最后,这里讨论的内容并不限于构造函数:任何以某种方式保留某个参数别名的函数,无论是把指针放进缓存,还是把参数绑定到分离函数中,都应该按指针接收该参数。
相关阅读
- 每周技巧 #5:消失术
- 每周技巧 #101:返回值、引用和生命周期
- 每周技巧 #176:优先使用返回值,而不是输出参数
- 每周技巧 #177:可赋值性与数据成员类型
- C++ Style Guide: Inputs and Outputs