6.3. 简易的 unique_ptr 实现

首先尝试实现一个简易版的 std::unique_ptr<T>,我们的目标是帮助读者理解实现此类指针所需的代码逻辑,而并非鼓励替代标准库实现 —— 标准智能指针经过充分测试且稳定可靠,应当优先使用。

6.3.1 类型签名

unique_ptr<T> 实际类型签名为 unique_ptr<T,D>,其中 D 默认为 default_deleter<T>。我们将同时实现标量版本和数组版本的特化,数组版本需要提供 operator[],而标量版本不应暴露该操作符。

首先是基础删除器实现,定义基础删除器类型(用户可自定义删除器,只需保持 operator() 签名一致):

namespace managing_memory_book {
  // 基本删除器类型
  template <class T>
  struct deleter_pointer_wrapper {
    void (*pf)(T*);
    deleter_pointer_wrapper(void (*pf)(T*)) : pf{ pf } {
    }
    void operator()(T* p) const { pf(p); }
  };

  template <class T>
  struct default_deleter {
    void operator()(T* p) const { delete p; }
  };

  template <class T>
  struct default_deleter<T[]> {
    void operator()(T* p) const { delete[] p; }
  };

  // ...
}

目前我们已实现三种调用方式相同,且均为类类型的删除器(这种统一性设计的原因稍后会明朗,但请注意保持一致性往往有其价值)。

其中较特殊的是 deleter_pointer_wrapper<T>,封装了可复制的状态(函数指针),但调用行为与其他两种删除器一致:当对 T* 调用时,会执行用户提供的函数操作。

下一步是确定 unique_ptr<T,D> 的实现形式。考虑到大多数删除器是无状态的,可利用空基类优化(EBO) —— 直接从删除器类型继承。唯一例外是,当删除器为函数指针时(因函数指针不能作为基类),此时改为从 deleter_pointer_wrapper<T> 继承。为了在这两种方案中选择,需要检测 D 是否为函数指针,这将通过自定义的 is_deleter_function_candidate<T> 类型特征实现。

检测函数指针类删除器的实现部分如下:

#include <type_traits>
namespace managing_memory_book {
  // ...
  template <class T>
  struct is_deleter_function_candidate
    : std::false_type {};

  template <class T>
  struct is_deleter_function_candidate<void (*)(T*)>
    : std::true_type {};

  template <class T>
  constexpr auto is_deleter_function_candidate_v =
    is_deleter_function_candidate<T>::value;
  // ...
}

这段代码的含义应该不言自明 —— 其核心思想是:大多数类型都不适合作为删除函数,但符合void(*)(T*)签名的函数指针类型除外。

接下来我们实现通用的unique_ptr<T>标量版本。通过之前实现的删除器函数检测特性,可以条件选择基类:对于普通删除器类型,直接继承D,对于函数指针类型,则继承deleter_pointer_wrapper<T>。

析构函数中,将基类指针转换为相应类型来释放资源:

namespace managing_memory_book {
  // ...
  // unique_ptr 通用模板
  template <class T, class D = default_deleter<T>>
  class unique_ptr : std::conditional_t <
    is_deleter_function_candidate_v<D>,
    deleter_pointer_wrapper<T>, D
  > {
    using deleter_type = std::conditional_t <
      is_deleter_function_candidate_v<D>,
      deleter_pointer_wrapper<T>,
      D
    >;

    T* p = nullptr;
  public:
    unique_ptr() = default;

    unique_ptr(T* p) : p{ p } {
    }

    unique_ptr(T* p, void (*pf)(T*))
      : deleter_type{ pf }, p{ p } {
    }

    ~unique_ptr() {
      (*static_cast<deleter_type*>(this))(p);
    }
  };
  // ...
}

本质上,同样的方法也适用于类型的T[]特化:

namespace managing_memory_book {
  // ...
  // unique_ptr 数组特化版
  template <class T, class D>
  class unique_ptr<T[], D> : std::conditional_t <
  is_deleter_function_candidate_v<D>,
    deleter_pointer_wrapper<T>,
    D
  > {
    using deleter_type = std::conditional_t <
      is_deleter_function_candidate_v<D>,
      deleter_pointer_wrapper<T>,
      D
    >;

    T* p = nullptr;
  public:
    unique_ptr() = default;

    unique_ptr(T* p) : p{ p } {
    }

    unique_ptr(T* p, void (*pf)(T*))
      : deleter_type{ pf }, p{ p } {
    }

    ~unique_ptr() {
      (*static_cast<deleter_type*>(this))(p);
    }
  };
}

默认的unique_ptr在概念上的行为类似于空指针,这对大多数人来说不足为奇。现在已经有了基本的概念,继续研究特定于unique_ptr的语义。

6.3.2 特殊成员函数

unique_ptr 标量版本和数组版本的特殊成员函数实现代码将保持一致。前文已探讨过析构函数和默认构造函数,现在成对分析其余四个函数:

通用版本及其数组特化版本的实现代码如下(代码使用了 <utility> 头文件中的 std::exchange() 和 std::swap()):

// ...
  unique_ptr(const unique_ptr&) = delete;

  unique_ptr& operator=(const unique_ptr&) = delete;

  void swap(unique_ptr &other) noexcept {
    using std::swap;
    swap(p, other.p);
  }

  unique_ptr(unique_ptr &&other) noexcept
    : p{ std::exchange(other.p, nullptr) } {
  }

  unique_ptr& operator=(unique_ptr &&other) noexcept {
    unique_ptr{ std::move(other) }.swap(*this);
    return *this;
  }
// ...

至此,大部分实现逻辑应该已经不言自明。这里使用了std::exchange(),其作用是:将other.p的值复制给this→p,然后将nullptr赋给other.p —— 这正符合所有权转移的预期行为。该类型的移动操作不仅简单高效,而且保证不会抛出异常,这两个特性都非常理想。

以下操作在通用版本和数组特化版本中都将实现:

这些操作的实现本质上都很简单。另一个需要提供的成员函数是get()(包括const和非const版本),用于向需要与系统调用等底层函数交互的使用端代码暴露底层指针:

// ...
  bool empty() const noexcept { return !p; }

  operator bool() const noexcept { return !empty(); }

  bool operator==(const unique_ptr &other)
    const noexcept {
    return p == other.p;
  }

  // 自 C++20 起,可由 operator==() 推导而来
  bool operator!=(const unique_ptr &other)
    const noexcept {
    return !(*this == other);
  }

  T *get() noexcept { return p; }

  const T *get() const noexcept { return p; }
// ...

自C++20起,只要operator==()提供了标准签名,就无需显式实现operator!=(),编译器会自动基于operator==()合成operator!=(),这一机制非常简洁。

现在,看看指针式操作函数operator*()、operator→()和operator[]()的实现方式。

6.3.3 指针式操作函数

标量版本和数组版本需要实现不同的指针式操作:

// ...
  T& operator*() noexcept { return *p; }
  const T& operator*() const noexcept { return *p; }
  T* operator->() noexcept { return p; }
  const T* operator->() const noexcept { return p; }
// ...

operator→() 成员函数是个特殊的存在:当作用于对象时,会不断在返回对象上被重调用(持续递归),直到最终返回原始指针,编译器才知道如何继续处理。

对于数组版本(unique_ptr<T[]>特化),需要实现operator[],这比实现operator*()或operator→()更符合逻辑:

// ...
  T& operator[](std::size_t n) noexcept {
    return p[n];
  }

  const T& operator[](std::size_t n) const noexcept {
    return p[n];
  }
// ...

这些成员函数存在看似重复的const和非const版本,这个“惯例”始于之前介绍的get()成员函数。虽然语法相似,但其语义截然不同:只有const版本可通过const修饰的unique_ptr<T>对象调用。

如果使用C++23编译器,通过编写适当的模板成员函数,可以让编译器自动合成实际使用到的版本:

// 以下实现同时适用于数组和非数组类型
template <class U>
decltype(auto) get(this U && self) noexcept {
  return self.p;
}

// 以下两个函数仅适用于非数组类型
template <class U>
decltype(auto) operator*(this U && self) noexcept {
  return *(self.p);
}

template <class U>
decltype(auto) operator->(this U && self) noexcept {
  return self.p;
}

// 以下函数仅适用于数组类型
template <class U>
decltype(auto) operator[](this U && self,
                          std::size_t n) noexcept {
  return self.p[n];
}

这一机制将需要编写的成员函数数量减半。其原理在于:C++23引入了“deduced this”特性,允许开发者用this关键字显式标记成员函数的第一个参数。结合转发引用(U&&类型),编译器可自动推导this的常量性(const-ness),从而用单个函数同时表达const和非const版本。这些函数使用了decltype(auto)返回类型,能根据return语句中的表达式自动推断返回类型的cv限定符(第3章讨论过)和引用属性。

至此,已经完成了一个简单但功能完备的unique_ptr<T>基础实现,可满足大多数使用场景。尽管unique_ptr<T>非常实用,但并非万能 —— 实际编程中还存在其他需求。接下来着手实现简化版的shared_ptr<T>,看看如何实现共享所有权语义。

使用自制的unique_ptr<T>和默认删除器的简单程序:

// ... (我们自己的unique_ptr<T>在这里…)
struct X {};

int main() {
  unique_ptr<X> p{ new X };
} // X::~X() called here

另一个使用自定义删除器的示例:

// ... (我们自己的unique_ptr<T>在这里…)
class X {
  ~X(){}
public:
  static void destroy(X *p) { delete p; }
};

int main() {
  unique_ptr<X, &X::destroy> p{ new X };
} // X::destroy(p.get()) 在这里调用