(原文链接:https://abseil.io/tips/116 译者:clangpp@gmail.com)
每周贴士 #116: 如何存储指向参数的引用
- 最初发布于:2016-05-26
- 作者:Alex Pilkiewicz
- 更新于:2020-06-01
- 短链接:abseil.io/tips/116
From painting to image, from image to text, from text to voice, a sort of imaginary pointer indicates, shows, fixes, locates, imposes a system of references, tries to stabilize a unique space. — This is Not a Pipe by Michel Foucault
(译者注:这段文字太高科技了,字都认识,连起来不认识,我就不关公面前耍AK47了,欢迎评论推荐译文)
常引用和常指针
作为函数参数,常引用比常指针多了几个优势:它们不可能为空,并且清楚地表明函数不会接管对象的所有权。但是它们之间还有些区别有时候让人挠头:它们更隐蔽(也就是说,调用端无从得知它是个引用),而且它们能绑定到临时变量上。
类中悬挂引用的风险
考虑如下的类作为例子:
class Foo {
public:
explicit Foo(const std::string& content) : content_(content) {}
const std::string& content() const { return content_; }
private:
const std::string& content_;
};
看起来科学啊。但如果我们用字符串常量构造一个Foo
会发生什么?
void Func() {
Foo foo("something");
std::cout << foo.content(); // 完蛋!
}
构造foo
的时候,由字符串常量构造的临时std::string
对象,传给了构造函数并绑定到了数据成员content_
上。这个临时的字符串在创建它的那行结束后离开了作用域(译者注:也即被析构)。现在foo.content_
引用指向的对象不再存在了。访问它是未定义行为,因此任何事情都可能发生,既可能在单元测试中完全正常,又可能在生产环境中错得离谱。
一种方案:使用指针
在我们的例子中,最简单的方法也许是按值传递和存储字符串。但是让我们先假设引用原始的函数参数是必须的,比如,因为它不是个字符串,而是个更好玩的类型。解决方案是以指针传递参数。
class Foo {
public:
// 别忘了这条注释:
// 不接管content的所有权,content必须指向一个生存期超过此对象的合法字符串。
explicit Foo(const std::string* content) : content_(content) {}
const std::string& content() const { return *content_; }
private:
const std::string* const content_; // 不拥有所有权,不能为空
};
现在下面的代码就会直接编译不过:
std::string GetString();
void Func() {
Foo foo1(&GetString()); // 错误:试图获取临时'std::string'变量的地址
Foo foo2(&"something"); // 错误:没有可匹配的'Foo'构造函数
}
而且调用端变得非常清晰,对象可能持有其参数的地址:
void Func2() {
std::string content = GetString();
Foo foo(&content);
}
更进一步,省个注释:存引用
你可能已经注意到了,我们同样的话说了两遍:指针不能为空,且不接管所有权。一次在构造函数的文档里,一次在实例变量的注释里。这是必要的吗?考虑如下代码:
class Baz {
public:
// 不接管任何所有权,且所有指针必须指向生存期超过此对象的合法对象。
explicit Baz(const Arg1* arg1, Arg2* arg2) : arg1_(*arg1), arg2_(*arg2) {}
private:
const Arg1& arg1_; // 现在很清楚了,我们不接管所有权,而且引用不会是空
Arg2& arg2_; // 对,非常引用(non-const reference)符合代码风格规范!
};
引用类型数据成员的一个缺点是,你没法对其重新赋值,也就是说你的类不会有拷贝运算符(构造函数不受影响,不过根据三法则,也许显示地删掉(delete)它更合理)。如果你的类需要允许重新赋值,那你需要非常指针(non-const pointer),它仍然可以指向常对象。
如果你想深层次地防守,担心有些调用者可能会不小心传进来空指针,你可以用*ABSL_DIE_IF_NULL(arg1)
引发一个崩溃(crash)。需要注意的是,与通常理解的不同,简单地解引用空指针,不一定会引发崩溃;相反地,这是未定义行为,不该依赖其结果。这里实际可能发生的事情是,因为引用是以指针实现的,它可能会被复制,然后在晚些时候,有人访问成员的时候崩溃。
结论
在以下情况下以常引用传递参数是可以的:参数被复制,或者只在构造函数内使用,并且构造的对象中不会存储指向它的引用。其他情况下,考虑以指针传递参数(常指针或非常指针)。还有,如果你实际上在传递一个对象的所有权,那应该使用std::unique_ptr
。
最后,这里讨论的问题不限于构造函数:任何函数以任何方式保存了其参数的别名,不论是把指针放进缓存,还是在派遣函数(detached function)中绑定了该参数,都应该以指针传递该参数。