6.5. 基于策略的复制指针

先搁置一下标准智能指针。假设现在需要实现一种既不符合std::unique_ptr<T>独占所有权语义,也不符合std::shared_ptr<T>共享所有权语义的智能指针类型。具体而言,想要实现单一所有权语义,但与不可复制但可移动的std::unique_ptr<T>不同,希望在复制指针时同时复制其指向的对象。

该如何实现?在C++中可以自定义实现。将这个新智能指针类型命名为up_ptr<T>(“duplicating pointer”的缩写,即“会复制所指对象的指针”)。由于本章前文已通过自实现的unique_ptr<T>探讨了独占所有权的实现方式,本节将重点讨论如何复制所指对象。

“复制”具体指什么?这里需要考虑两种典型情况:复制非多态类型的对象和复制多态类型的对象(本例中将“多态”定义为“至少包含一个虚函数”)。当然,富有创造力的开发者们总会遇到更特殊的情况,因此我们将重点处理上述“典型情况”,同时为特殊应用场景保留扩展可能。

为何要区分多态与非多态类型?请看以下示例程序:

struct X { int n; };

struct B {
  int n;
  B(int n) : n{ n } {}
  virtual ~B() = default;
};

struct D0 : B {
  D0(int n) : B{ n } { /* ... */ }
// ...
};

struct D1 : B {
  D1(int n) : B{ n } { /* ... */ }
  // ...
};

// 前提条件:p != nullptr(为了简化逻辑)
X* duplicate(X *p) {
  return new X{ *p }; // 正确:复制一个 X 对象
}

// 前提条件:p != nullptr(为了简化逻辑)
B* duplicate(B *p) {
  return new B{ *p }; // 危险操作!
}

#include <memory>

int main() {
  using std::unique_ptr;
  X x{ 3 };
  unique_ptr<X> px { duplicate(&x) };
  D0 d0{ 4 };
  unique_ptr<B> pb{ duplicate(&d0) }; // 麻烦来了
}

可以假设duplicate(X*)函数能安全创建X类型对象,X没有虚成员函数,所以很可能不作为公共基类使用。但对于duplicate(B*),直接调用B的构造函数大概率是错误的 —— 传入的B*可能指向B对象,也可能指向派生类对象(如这里的D0)。此时调用new B{ *p };只会构造基类部分,导致派生类状态被截断,最终可能产生错误结果。

面向对象编程领域众所周知,复制多态类型对象的常规方法是采用主观复制(subjective duplication),即克隆机制(cloning)。通俗地说,当持有带虚函数的对象指针时,唯一能真正确定所指对象类型的实体……就是该对象本身。

因此,dup_ptr<T>将根据T的特征选择复制策略:默认情况下,若T是多态类型,则通过克隆复制;否则通过常规复制复制。当然,我们允许使用端代码在需要时指定自定义复制机制。

这里探讨三种默认复制策略的选择方案:

6.5.1 通过接口检测

在用户代码中,可以强制要求可克隆类型实现特定接口:

struct cloneable {
  virtual cloneable * clone() const = 0;
  virtual ~cloneable() = default;
}

这种方案可能达不到标准化要求,因其具有侵入性,会带来一定开销(假设可克隆类型都是多态类型,这虽然常见但非强制要求)等。当然,在自有代码库中这仍不失为一种解决方案。将这个思路应用到之前处理多态类型复制不当的示例中,可以得到如下改进:

// ... 可克隆类型
struct X { int n; };
struct B : cloneable { // 每个 B 都是可克隆的
  int n;

  B(int n) : n{ n } {}

  virtual ~B() = default;

  B * clone()

protected: // 可克隆类型以特定(主观)方式复制
  B(const B&) = default;
};

struct D0 : B {
  D0(int n) : B{ n } { /* ... */ }
  D0* clone() const override { return new D0{ *this }; }
  // ...
};

struct D1 : B {
  D1(int n) : B{ n } { /* ... */ }
  D1* clone() const override { return new D1{ *this }; }
  // ...
}

假设要开发一个dup_ptr<T>框架,能对非继承自Cloneable的类型执行复制,对继承自Cloneable的类型执行克隆。为此,可以使用std::conditional类型特征,在两种函数对象类型之间选择 —— 执行复制的Copier类型和执行克隆的Cloner类型:

// ... 可克隆类型
struct Copier {
  template <class T> T* operator()(const T *p) const {
    return new T{ *p };
  }
};
struct Cloner {
  template <class T> T* operator()(const T *p) const {
    return p->clone();
  }
};

#include <type_traits>

template <class T,
          class Dup = std::conditional_t<
            std::is_base_of_v<cloneable, T>,
            Cloner, Copier
          >>
class dup_ptr {
  T *p{};
  // 当需要复制时,使用 Dup 类型的对象:
  // 包括复制构造函数和复制赋值操作符
  // ...
public:
  dup_ptr(const dup_ptr &other)
    : p{ other.empty()? nullptr : Dup{}(other.p) } {
  }
  // ...
}

该实现假定Dup类型是无状态的(没有成员变量),但在实际应用中应当予以明确说明(如果允许有状态的Dup类型,就需要实例化Dup对象并编写复制和移动该对象的代码,这将导致实现复杂度大幅增加)。在此实现下,继承自cloneable的类型都将执行克隆操作,其他类型则执行复制操作 —— 除非用户代码提供了特殊的Dup类型实现。

6.5.2 通过特征(traits)检测

如果不希望强制可克隆类型继承基类,可以利用类型特征(type traits)来检测是否存在const限定的clone()成员函数,并据此合理推断克隆比复制更适合,这种非侵入式方法隐含约定了clone()的语义一致性。

实现方式有多种,其中最清晰通用的方案当属Walter Brown博士提出的std::void_t类型(自C++17起定义于<type_traits>中):

// types Cloner and Copier (see above)
template <class, class = void>
  struct has_clone : std::false_type { };

  template <class T>
  struct has_clone <T, std::void_t<
    decltype(std::declval<const T*>()->clone())
  >> : std::true_type { };

  template <class T>
  constexpr bool has_clone_v = has_clone<T>::value;

  template <class T, class Dup = std::conditional_t<
    has_clone_v<T>, Cloner, Copier
  >> class dup_ptr {
    T *p{};
  public:
    // ...
    dup_ptr(const dup_ptr &other)
      : p{ other.empty()? nullptr : Dup{}(other.p) } {
    }
    // ...
};

std::void_t 是一项精妙的发明,让有经验的开发者能够以有限但通用的方式模拟 C++20 的 requires 表达式功能。这个示例的工作原理如下:

选定 Dup 类型后,后续操作与之前相同。这种实现相比前一种方案的优势在于:

关于std::void_t一词

std::void_t 堪称一项精妙绝伦的设计,核心原理是利用“替换失败并非错误”(SFINAE)机制,在基础通用实现(返回“否”)与特化版本(当特定表达式合法时返回“是”)之间进行选择。以我们的has_clone<T>为例:

  • 对大多数类型返回false

  • 仅当const T*类型对象p能合法调用p→clone()时返回true

这种能在概念(concepts)成熟之前就实现任意表达式合法性检测的机制实在令人叹服,我们不得不感谢Walter Brown博士贡献的这一杰作(以及他的诸多其他贡献)。

6.5.3 通过概念(concepts)检测

自 C++20 起,像 std::void_t 这样的技巧不再像以前那样有用, concepts 现在已成为语言类型系统的一部分。通过 concepts,我们可以将一个可克隆类型 T 定义为:对于该类型,对 const T* 调用 clone() 是良构的(well-formed),并且其返回结果可以转换为 T*。

因此可得到如下定义:

template <class T>
concept cloneable = requires(const T *p) {
  { p->clone() } -> std::convertible_to<T*>;
};

template <class T, class Dup = std::conditional_t<
          cloneable<T>, Cloner, Copier
>> class dup_ptr {
  T *p{};
public:
  // ...
  dup_ptr(const dup_ptr &other)
    : p{ other.empty()? nullptr : Dup{}(other.p) } {
  }
  // ...
}

概念(Concepts) 和 特性(traits) 一样,都是解决此类问题的非侵入式方案。但与特性是一种编程技巧不同,概念已经深入集成到类型系统中,可以(例如)编写针对 cloneable<T> 的特化代码,也可以编写不依赖它的通用代码。在当前的场景中,如果希望为那些既不使用复制构造函数,也不提供 clone() 成员函数的类型保留扩展的可能性,则表明:目前这种允许使用端代码提供其他复制机制的设计方案,可能是更优的选择。

C++26

C++26 将包含两个标准类型,名为 std::indirect 和 std::polymorphic,将涵盖与本文所述 dup_ptr 所类似的一个特定用途。该提案已于 2025 年 2 月 15 日获得通过。