Google C++每周贴士 #116: 如何存储指向参数的引用

(原文链接:https://abseil.io/tips/116 译者:clangpp@gmail.com)

每周贴士 #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)中绑定了该参数,都应该以指针传递该参数。

相关阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值