先搁置一下标准智能指针。假设现在需要实现一种既不符合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是多态类型,则通过克隆复制;否则通过常规复制复制。当然,我们允许使用端代码在需要时指定自定义复制机制。
这里探讨三种默认复制策略的选择方案:
基于接口的侵入式方案
基于类型特征(traits)的非侵入式方案(使用C++17特性检测克隆成员函数)
基于C++20概念(concepts)的非侵入式方案
在用户代码中,可以强制要求可克隆类型实现特定接口:
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类型实现。
如果不希望强制可克隆类型继承基类,可以利用类型特征(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 表达式功能。这个示例的工作原理如下:
一般情况下,has_clone
对于 p→clone() 调用(其中 p 是 const T* 类型对象)的 T 类型,has_clone<T>::value 为 true
选定 Dup 类型后,后续操作与之前相同。这种实现相比前一种方案的优势在于:
当前方案检测是否存在符合要求的 clone() 成员函数
前一种方案则检测是否存在特定基类
实现一个函数比继承特定基类施加的约束更轻量
std::void_t 堪称一项精妙绝伦的设计,核心原理是利用“替换失败并非错误”(SFINAE)机制,在基础通用实现(返回“否”)与特化版本(当特定表达式合法时返回“是”)之间进行选择。以我们的has_clone<T>为例:
对大多数类型返回false
仅当const T*类型对象p能合法调用p→clone()时返回true
这种能在概念(concepts)成熟之前就实现任意表达式合法性检测的机制实在令人叹服,我们不得不感谢Walter Brown博士贡献的这一杰作(以及他的诸多其他贡献)。
自 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 将包含两个标准类型,名为 std::indirect 和 std::polymorphic,将涵盖与本文所述 dup_ptr 所类似的一个特定用途。该提案已于 2025 年 2 月 15 日获得通过。