每周小贴士#149:对象生命周期与=delete

Posted -飞鹤-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每周小贴士#149:对象生命周期与=delete相关的知识,希望对你有一定的参考价值。

作为TotW#149最初发表于2018年5月3日

由Titus Winters创作

钱花光之后,就消失了。一生一次,水在地下流。——大卫.伯恩

关于生命周期的=delete

想像一样,你有一个需要引用某个长期生存对象的API,但不拥有它的所有权。

class Request 
  ...

  // 提供的上下文必须与当前请求有一样的生存期
  void SetContext(const Context& context);
;

你自己想,“嘿,如果有人传递临时变量会发生什么?那将是一个错误。但这是现代C++,我可以阻止它!”因此,您在 API 中进行了更改,添加了已删除的重载。

class Request 
  ...

  // 提供的上下文必须与当前请求有一样的生存期
  void SetContext(const Context& context);
  void SetContext(Context&& context) = delete;
;

对你的工作感到满意,你在想“嘿,现在API讲出了所有事,注释是不需要的。”

class Request 
  ...

  void SetContext(const Context& context);
  void SetContext(Context&& context) = delete;
;

这是一个好主意吗?为什么是或为什么不是呢?

不能凭空进行设计

正如所呈现的,你可能认为这是一个好主意。然而,在许多API设计的示例中,查看API的定义是很有吸引力的,但是查看API如何使用更有用处。因此,让我们重看此方案,考虑用法。

一个用户试图使用原始的SetContext(),试图得到一些简单的构建但不知道在哪里可以找到正确的Context对象,只是进行了建议的调用。

request.SetContext(Context());

没有你=delete的改变,这里会构建,但会在运行时失败(可能以一种神秘的方式)。查看SetContextAPI 时,会记录生命周期要求,并更改代码以符合要求。

request.SetContext(request2.context());

另一方面,尝试对SetContext()你的更改使用“改进”且不加评论的用户首先遇到构建中断:=delete

错误: 调用已删除的成员函数“SetContext” 

  request.SetContext(Context());
  ~~~~~~~~^~~~~~~~~~

:4:8: 注意:候选函数已经显式删除
  void SetContext(Context&& context) = delete;

然后用户会想“好吧,我不能传递临时变量”,但是没有关于实际需求的信息,最有可能的修复是什么?

Context context;
request.SetContext(context);

现在,问题的症结在于:新的自动变量的作用域有多大可能是context此调用的正确生命周期?如果您的答案低于 100%,则仍然需要生命周期需求的注释。

class Request 
  ...

  // 提供的上下文必须与当前请求有一样的生存期
  void SetContext(const Context& context);
  void SetContext(Context&& context) = delete;
;

以这种方式删除重载集的成员充其量只是一种折衷措施。是的,你避免了一类错误,但你也使 API 复杂化。依赖这样的设计是获得错误安全感的必经之路:C++类型系统根本无法编码有关参数生命周期要求的必要细节。

由于类型系统实际上无法做到这一点,我们建议您不要半途而废。保持简单——不要试图依赖这种模式来禁止临时对象,它的效果不够好。

=delete的优化

让我们翻转一下情形:可能你并不想阻止临时变量,可能你想阻止拷贝。

future<bool> DnaScan(Config c, const std::string& sequence) = delete;
future<bool> DnaScan(Config c, std::string&& sequence);

你的 API 的调用者永远不需要保留其值的可能性有多大?如果你不能 100% 确定你确切知道你的 API将如何使用,那么这就是惹恼您的用户的一个方法。考虑制作副本并在给定正常(未删除)设计的情况下调用此类 API:

Config c1 = GetConfig();
Config c2 = GetConfig();
std::string s = GetDna();

// 开始扫描两个配置
auto scan1 = DnaScan(c1, s);
auto scan2 = DnaScan(c2, std::move(s));

由于我们看到第二次扫描是最后一次使用s,所以我们可以直接std::move进入值消耗调用。使用“巧妙优化”的版本,代码看起来更加草率。

Config c1 = GetConfig();
Config c2 = GetConfig();
std::string s = GetDna();
std::string s2 = s;

// 开始扫描两个配置
auto scan1 = DnaScan(c1, std::move(s));
auto scan2 = DnaScan(c2, std::move(s2));

API被提供作为构建块和抽象,API生态系统是一个平台,可以以新的和令人惊讶的方式组装在一起,这比任何单个 API的提供者可能预测的要多。相信你肯定知道没有人应该制作副本与此背道而驰。此外,移动时的低效和复制问题比任何单个API都要广泛得多,并且可能通过分析、培训、代码审查和静态分析的某种组合来更好地解决。

在极少数情况下,当您可以确定必须以特定方式使用 API时:您可能应该在相关类型中对其进行编码。不要将std::string其作为您对 DNA 序列的表示进行操作;编写一个Dna类并使用显式(易于扫描)的方式使其只能移动以执行昂贵的复制操作。换句话说:类型的属性应该在这些类型中表达,而不是在操作它们的API中。

引用限定符

作为旁注:可以对破坏性访问器的引用限定符应用相同的推理。考虑一个类似的类std::stringbuf——在 C++20 中,它将获得一个访问器来使用包含的字符串,以现有访问器的重载集形式呈现:

const std::string& str() const &;
std::string str() &&;

(有关引用限定方法和重载集的更多信息,请参阅小贴士#148)。查看 的现有用法std::stringbuf,几乎每种用法都有一个stringbuf用于生成一个字符串的单一用法。忽略遗留代码,强制执行它并仅提供“有效”的限定引用的构造方法是好的方法吗?

当然不是,出于与DnaScan上面示例类似的原因:你无法确定没有人需要它,并且提供 const 重载并非不安全。仅将引用限定符用作优化的重载集,或者当引用限定符是强制语义正确性所必需的。

总结

尝试结合使用右值引用或引用限定符=delete来提供更“用户友好”的 API,强制执行生命周期或防止优化问题是很吸引人的。实际上,这些通常是不好的吸引。生存期要求比 C++ 类型系统可以表达的要复杂得多。API 提供者很少能预测其 API 的每一次未来有效使用。避免这些类型的=delete技巧可以使事情变得简单。

以上是关于每周小贴士#149:对象生命周期与=delete的主要内容,如果未能解决你的问题,请参考以下文章

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

本周小贴士#101:返回值,引用和生命周期

每周小贴士#65:就地安放

多线程与对象的生命周期管理

每周小贴士#152:AbslHashValue和你

每周小贴士#153:不要使用using指令