标准智能指针如unique_ptr<T>(独占所有权)和shared_ptr<T>(共享所有权)已为我们所熟知,而面对特殊场景时也能自定义实现(如前文探讨的dup_ptr<T>,它具备独占所有权特性,但在指针复制时会同步复制所指对象)。那么,还有哪些常见语义值得我们通过类型系统来强化呢?至少有两个显而易见的选项:实现“永不为空”的语义约束,以及实现“仅观察”的语义约束。
回顾之前的一个示例代码:
// ...
// 前提条件:p 不为空指针(为了简化逻辑)
X* duplicate(X *p) {
return new X{ *p }; // 没问题
}
// ...
请注意代码中的注释 —— 将“不传入空指针”的责任完全交给了使用者。可以通过多种方式强化这个约束:
使用assert(!p)
当!p时调用std::abort()
当!p时调用std::terminate()
当!p时抛出异常等
关键在于:如果确实需要确保指针非空,却只在运行时通过if(!p)进行检查,这很可能(或者说应该?)是设计上的失误 —— “只接受非空指针”这个约束本应通过类型系统来保证,代码本身的约束力永远强于注释说明。
这种设计理念已应用于某些商业库(比如,主流编译器厂商提供的指南支持库中的gsl::non_null<T>)。只要能明确定义错误处理方式,实现这种类型并不困难。为便于演示,将采用抛出异常作为错误处理机制:
class invalid_pointer {};
template <class T>
class non_null_ptr {
T *p;
public:
non_null_ptr(T *p) : p{ p } {
if (!p) throw invalid_pointer{};
}
T* get() const { return p; }
constexpr operator bool() const noexcept {
return true;
}
// ...
通过使用non_null_ptr<T>类型,接受该类型参数的函数都能确保其中的T*指针非空,从而免除了使用端代码的校验负担。这使得non_null_ptr<T>成为需要非空指针参数函数接口的理想选择。
该类的其余部分实现起来大多较为简单。关键特性在于:non_null_ptr<T>不提供默认构造函数,该构造函数需要将p数据成员初始化为某个默认值(很可能是nullptr),但这与“非空指针”的类型语义矛盾。
使用示例如下所示:
struct X { int n; };
class invalid {};
int extract_value(const X *p) {
if(!p) throw invalid{};
return p->n;
}
#include <iostream>
int main() try {
X x{ 3 };
std::cout << extract_value(&x) << '\n'
<< extract_value(nullptr) << '\n';
} catch(invalid) {
std::cerr << "oops\n";
}
现在将其与以下代码对比(假设non_null_ptr<T>在构造时传入空指针会抛出异常):
// non_null_ptr 类型的定义(此处省略)
struct X { int n; };
int extract_value(const non_null_ptr<X> &p) {
return p->n; // 定义一个简单结构体 X,包含一个整型成员 n
}
#include <iostream>
int main() try {
X x{ 3 };
std::cout << extract_value(&x) << '\n'
<< extract_value(nullptr) << '\n';
} catch(...) {
std::cerr << "oops\n";
}
相比直接使用T*,non_null_ptr<T>在此场景下的两大优势在于:
类型系统能更明确地表达设计意图(T*可能允许空指针,而non_null_ptr
调用函数无需验证指针有效性(验证已内置于类型系统)使用比T*更丰富的类型,能使调用方和被调用方的代码都更加健壮。
若被调用函数确实需要T*(需要调用C函数时),可通过non_null_ptr<T>的get()成员函数获取。毕竟C++向来注重实用性。
实现一个极简的智能指针observer_ptr<T>,其核心语义在于表明这个“智能”指针实际上不具备指针特性 —— 即限制原生指针支持的部分操作。典型场景是:对T*执行delete是合法操作,但对observer_ptr<T>执行delete则应禁止,因为observer_ptr<T>本质上是…非指针类型。请看以下示例:
class X { /* ... */ };
void f(X *p) {
// 使用 *p
// 我们传给 f() 的是一个裸指针,因此 f() 应该只是观察(使用)该指针,
// 而不应该拥有它(即不应该负责释放资源)
delete p; // 等等!你不应该这么做!
}
正如注释所言,而有读者可能会说:“但这个函数不该这么做!它并不拥有*p的所有权!”。然而,错误和误解总是难以避免。在这种情况下,由于参数类型本身并未表明“对p执行delete操作是非法的”,使得误解带来的后果更加严重。
现在让我们稍作修改:
class X { /* ... */ };
void f(observer_ptr<X> p) {
// 使用 *p
// delete p; // 不行,这段代码无法通过编译
}
两个版本中“使用*p”的注释保持相同。observer_ptr<T>类型提供了所有合理操作符和成员函数(get()、operator*()、operator→()、empty()等)的基础实现,因此在用户代码中使用T*和observer_ptr<T>应该基本等效 —— 唯一的区别在于禁止使用错误用法(如执行delete或指针运算)。
有时,仅仅在函数接口中明确意图就能显著提升代码质量。这种类型的主要价值在于:
通过类型系统明确表达“仅观察”语义
防止意外的所有权操作
保持与原生指针近似的使用体验
编译期捕获非法操作