本周小贴士#116: 保留对参数的引用

Posted -飞鹤-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了本周小贴士#116: 保留对参数的引用相关的知识,希望对你有一定的参考价值。

作为totW#116最初发表于2016年5月26日

由Alex Pilkiewicz创作

更新于2020年6月1日

从绘画到图像,从图像到文本,从文本到声音,一种虚构的指针指示、展示、固定、定位、强加引用系统,试图稳定一个独立空间。——米歇尔·福柯创作的这不是一只烟斗

const引用与指向const的指针

const引用用作函数的参数,与指向const的指针相比,有几个优点:它们不能为null,并且很明显该函数没有取得对象的所有权。但是它们有其他的有时会出现问题的差异:它们更加隐式(即在调用点没有任何内容表明我们正在接受引用),并且它们可以被绑定到临时变量。

类中悬空引用的风险

将以下类作为示例:

class Foo {
 public:
  explicit Foo(const std::string& content) : content_(content) {}
  const std::string& content() const { return content_; }

 private:
  const std::string& content_;
};

它看起来是合理的。但是如果我们从string字面量构建一个Foo会发生什么呢?

void Func() {
  Foo foo("something");
  std::cout << foo.content();  // 轰隆!
}

当创建foo时,content_成员被绑定到临时std::string对象,该对象创建自字面量并传递给构造函数。临时字符串在创建的那一行结尾就离开了作用域。现在foo.content_是对一个已经不存在的对象的引用。访问它是未定义的行为,任何事情都可能发生,在测试中工作正常,到实际生产中出现错误。

解决方案:使用指针

在我们的示例中,最简单的方案可能是按值来传递和保存string。但是假定我们需要参考原始的参数,例如它不是一个string,而是一些更有趣的类型。解决方案是按指针传递参数:

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_;  // 是的,非常量引用是合适的风格。
};

引用类型的成员的一个缺点是不能重新赋值,意味着你的类没有一个拷贝赋值运算符(拷贝构造函数是正常的),但是显式地删除它以遵循3规则可能是有意义的。如果你的类应该是可分配的,那么你需要一个非常量指针,仍旧指向一个常量对象。

如果你想深入防御并认为调用者可能意外地传递一个空指针,那么可以使用*ABSL_DIE_IF_NULL(arg1)来产生一个崩溃。请注意,仅仅解引用空指针并不像通常认为的那样会保证崩溃;相反它是未定义行为并且不应该被依赖。这里可能发生的是,当有人实际访问这个字段时,由于引用被作为指针来执行,它将被拷贝并且随后将崩溃。

结论

如果参数是拷贝的,或者只是在构造函数中使用并且未在构造函数对象中保留它的引用,那么通过常量引用传递参数到构造函数是可行的。在其他情况中,考虑以指针传(无论是否const)递参数。还请记住,如果你实际正在传递对象的所有权,则应该将其作为std::unique_ptr来传递。

最后,此处讨论的内容不限定于构造函数:以某种方式保留参数之一的别名的函数,无论是通过放置一个指针在缓存中还是将参数绑定到分离函数中,都应该通过指针获取该函数。

以上是关于本周小贴士#116: 保留对参数的引用的主要内容,如果未能解决你的问题,请参考以下文章

本周小贴士#116: Using声明和命名空间别名

本周小贴士#116: Using声明和命名空间别名

本周小贴士#116: 拷贝消除与值传递

本周小贴士#116: 拷贝消除与值传递

本周小贴士#94:调用点可读性和bool参数

本周小贴士#107:引用生命周期的扩展