[译] GotW #104: Smart Pointers, Part 2 (Difficulty: 5/10)

[译] GotW #104: Smart Pointers, Part 2 (Difficulty: 5/10)

当你正在研究最近新加入的项目的代码时,你发下了下面的工厂方法的声明:

widget* load_widget( widget::id desired );

JG 问题

1. 返回值的类型有什么问题?

 

Guru 问题

2. 返回值的推荐类型是是什么? 解释你的答案,包括任何的权衡。

3. 你希望更改返回值的类型为问题 #2 中推荐的类型,但是你担心破坏了原有代码的函数调用的兼容性;重新编译现有的函数调用是可行的,但修改所有的函数调用就……。然后你灵光一闪,意识到这是一个相当新的项目,所有的代码都使用现代 C++ 惯用法,然后你继续修改返回值类型,而没有一点担心,知道重构仅仅需要一点点或者更不不需要更改调用函数的代码。是什么让你如此自信?

 

解决方案

1. 返回值的类型有什么问题?

第一,我们能够从两行的问题描述中知道些什么?

  • 我们说过,load_widget 是一个工厂方法。它通过“loading” 生产出一个对象,然后把它返回给调用者。由于返回值类型是指针类型,所以结果可能是 null。
  • 调用者会适当的使用该对象,可能是调用它的成员函数,或者把它传给其他的函数,等等。这是不安全的,除非调用者确保该对象存活——调用者管理对象的生命周期;或者共享所有权,如果工厂方法负责维护内部的强引用和弱引用的话。
  • 因为调用者持有或共享所有权,它必须在对象不再需要时做一些工作。如果是直接持有,掌管生命周期的话,它应该销毁该对象,通常是一个 delete 或者也可能是类似于 unload_widget 的一个函数调用。不然的话,如果是共享所有权,它应该减少共享的引用计数。

不幸的是,返回一个 widget* 有两个主要的问题。第一,默认情况下它是不安全的,因为默认的操作模式(比如不保存返回值)会造成 widget 泄露:

// Example 1: Leak by default. Really, this is just so 20th-century...
//
 
widget* load_widget( widget::id desired );
 
:::
 
load_widget( some_id ); // oops

Example 1 的代码可以干净的编译,运行,(不)开心的泄露 widget

第二,函数签名传达出来的信息太少了,“一个 widget ?是的,开始吧!享受它。”文档中会指出调用者应该持有该对象,掌管对象的生命周期(或者其它),但是函数声明没有给出这些信息——它掌管对象的生命周期,还是共享所有权,到底是哪个?阅读文档然后记下它吧,因为函数声明没办法告诉我们到底是哪种情况。

 

2. 返回值的推荐类型是是什么? 解释你的答案,包括任何的权衡。

推荐的返回值类型是 unique_ptr 或者也可能是 shared_ptr。注意现在 C++ 中返回值类型是很平常的了,但是还有一点不太合适,它改变了语义,因为返回指针类型允许返回 null (无对象),而返回一个值得话不能轻易地处理这个语义,除非加个类似于 optional<> 的东西。

准则:工厂方法应该默认返回 unique_ptr 类型,或当需要共享所有权时使用 shared_ptr 类型。

这同时解决了上面两个问题:安全,自描述。

第一,考虑如何立即解决 Example 1 中的安全问题:

// Example 2: Clean up by default. Much better...
//
 
unique_ptr<widget> load_widget( widget::id desired );
 
:::
 
load_widget( some_id ); // cleans up

Example 2 的代码可以干净的编译,运行,开心的清理 widget。但它不仅仅在这种情况下正确——构造也是正确的,因为现在没有办法去制造出一个导致泄露的错误。

画外音:可能会有人说,“难道不会有人一直写 load_widget(some_id).release()?”如果这些人发神经的话,当然可以;正确的答案是,“不要这么做。” 记住我们关心的是什么——bug 和错误,而不是精心设计的犯罪——显然病态的滥用属于后一类。这与在 C# 的 using 块中显式的调用 Dispose,或在 Java 的 try-with-resources 块中调用 close,没什么区别,不会比它们存在更多的类型安全问题。

如果清理工作不是一个单独的 delete 语句,那么该怎么做?简单:使用自定义的 deleter。锦上添花的是例子中的工厂方法知道使用哪个 deleter,并且能够在构造返回值的时候确定;调用者不需要关心这些,尤其是,当调用者使用 auto 关键字作为返回值类型的时候。

 

第二,这是自描述的:一个返回值类型是 unique_ptr 的函数,清楚的说明这是一个纯“source” 函数,如果一个函数的返回值是 shared_ptr,那么清楚的说明它返回的是一个共享的所有权和/或一个观察者。

最后,为什么默认情况下,如果你不需要表达共享所有权的语义时,优先考虑使用unique_ptr?因为不管对于性能还是正确性来说,这都是对的事情,并且也给调用者留下了一些余地:

  • 返回值类型为 unique_ptr 表示返回唯一所有权,这是纯“source” 工厂方法的标准形式。
  • unique_ptr 的性能不会被打败——转移一个 unique_ptr 和转移/复制一个原始指针一样的廉价。
  • 如果调用者想要通过 shared_ptr 来管理对象的声明周期,可以通过隐式的转移操作来转换为 shared_ptr 类型——不需要显式的 std::move 因为编译器知道返回值是一个临时对象。
  • 如果调用者想要用其他的办法来维护对象的生命周期,可以通过调用 .release() 来得到原始指针。这是有用的,但是 shared_ptr 没有。

看下面的实现:

// Example 2, continued
//
auto up = load_widget(1);                              // unique_ptr (by default)
shared_ptr<widget> sp = load_widget(2);                // shared_ptr (if desired)
my::smart_ptr<widget> msp = load_widget(3).release();  // your own smart pointer (if desired)
widget* p = load_widget(4).release();                  // or even old-school manual management (not recommended)
delete p;

当然,如果工厂方法持有一些共享所有权的对象,不论是通过内部的 shared_ptr 还是 weak_ptr,返回 shared_ptr。这时调用者只能被迫使用 shared_ptr,但在这种情形下,这样是合适的。

 

3. 你希望更改返回值的类型为问题 #2 中推荐的类型,但是你担心破坏了原有代码的函数调用的兼容性;重新编译现有的函数调用是可行的,但修改所有的函数调用就……。然后你灵光一闪,意识到这是一个相当新的项目,所有的代码都使用现代 C++ 惯用法,然后你继续修改返回值类型,而没有一点担心,知道重构仅仅需要一点点或者更不不需要更改调用函数的代码。是什么让你如此自信?

现代可移植的 C++ 代码使用 unique_ptrshared_ptr,和 auto。返回 unique_ptr 类型可以和这三个协同工作,返回 shared_ptr 只能与后两个协同工作。

如果调用者接受返回值时使用了 auto,例如 auto w = load_widget(whatever);那么类型自然会是正确的,正常的解引用也可以工作,只有在调用者试图把它存储到一个其他类型的非局部变量中时代码才会出问题。

准则:优先考虑使用 auto 声明变量,除非需要显式的类型转换。它更短,而且可以避免类型轻微更改时引起的不必要的波澜。

否则:如果调用者没有使用 auto ,那么它应该已经使用返回结果初始化了 unique_ptrshared_ptr, 因为现代 C++ 代码不使用非参数的原始指针变量(下次详细讨论)。每种情况下,返回一个 unique_ptr 可以工作:一个 unique_ptr 可以无缝的转换成这些类型,如果语义上需要返回共享的所有权,那么调用者应该已经使用了 shared_ptr,这样的话,再次工作还会是正常的。(可能比之前的更好,因为为了让原本的返回原始指针的版本正常工作,返回类型大概会被 enable_shared_from_this 所胁迫,但如果我们显示的返回一个 shared_ptr 的话,这些就是不需要的了。)

你可能感兴趣的:([译] GotW #104: Smart Pointers, Part 2 (Difficulty: 5/10))