当需要通过类型系统表达对类型T资源的所有权时,像unique_ptr<T>和shared_ptr<T>这样的智能指针表现出色。这是否说明T*变得毫无用处?当然不是。
首先,对于函数而言,接收T*作为参数应该表示该函数是该T的观察者(observer)而非所有者(owner)。如果代码库以这种方式使用原始指针,基本上不会遇到问题。
其次,可以在实现首选所有权语义的类内部使用原始指针。只要容器实现了清晰的复制和移动语义,使用原始指针来实现操作对象的容器(用于各种遍历顺序的树状结构)完全可行。需要避免的是将容器内部节点的指针暴露给外部代码,并要特别注意容器的接口设计。
请看下面这个单向链表的示例(节选):
template <class T>
class single_linked_list {
struct node {
T value;
node *next = nullptr;
node(const T &val) : value { val } {
};
node *head = nullptr;
// ...
public:
// ...
~single_linked_list() {
for(auto p = head; p;) {
auto q = p->next;
delete p;
p = q;
}
}
};
我们将在第13章更详细地探讨这个例子。该析构函数工作正常(假设类的其他部分也编写合理),这个类是可用的且实用的。现在,假设使用unique_ptr<node>而不是node*作为single_linked_list的head数据成员,并替换node的next成员。这表面上看是个好主意,但需要考虑其后果:
template <class T>
class single_linked_list {
struct node {
T value;
unique_ptr<node> next; // 好主意?
node(const T &val) : value { val } {
};
unique_ptr<node> head; // 好主意?
// ...
public:
// ...
~single_linked_list() = default;
}
这表面上看似合理,但并没有传达正确的语义 —— 实际上一个节点并不拥有也不负责下一个节点。我们不希望移除一个节点时导致后续节点(以及之后的节点)递归销毁。如果这在single_linked_list的析构函数中看起来像是一种简化,请思考其后果 —— 这种策略会导致链表中有多少个节点,就会递归调用多少次析构函数,这是触发栈溢出的绝佳方式!
使用智能指针时要确保其语义与实际使用场景匹配。当指针所建模的关系既不是唯一所有权也不是共享所有权时,可能不需要提供这些语义的智能指针类型,转而使用非标准的智能指针,或者直接使用原始指针更为合适。
关键点在于:智能指针应该反映真实的所有权关系。在链表这种结构中,节点间的连接是纯粹的结构性关系,而非所有权关系,因此原始指针才是更合适的选择。这种设计既避免了不必要的所有权约束,又保持了数据结构的操作灵活性。
最后,在处理底层接口时(如系统调用),往往必需使用原始指针。但这并不代表编写系统级代码时,不能使用高级抽象(如vector<T>或unique_ptr<T>) —— 可以通过vector<T>的data()成员函数获取底层数组指针,也可以通过unique_ptr<T>的get()成员函数获取其管理的原始指针。只要语义合理,完全可以将调用代码视为在调用期间“借用”调用方的指针资源。
当别无选择时,使用原始指针也无妨 —— 它们本就为此而存在。但请谨记:尽可能使用高级抽象。这不仅能使代码更简洁安全,往往(超乎想象)还能提升性能。若暂时无法明确高级语义,或许说明相关代码的编写时机尚未成熟 —— 多花时间思考这些语义问题,最终会获得更好的设计成果。