适用于您大部分代码的智能指针包装






4.54/5 (9投票s)
介绍单一所有者的智能观察者以及公共和私有范围可见性的概念。
引言
这是一个简单的智能指针库,它提供了许多人在使用 std::shared_ptr
/weak_ptr
或垃圾回收时所追求的指针安全性,但它基于单一所有权模型,并保留了单一所有权的所有优点。它牢固地基于两种范式:
- 单一所有者的智能观察者(在之前的两篇文章 XONOR 指针:独占所有权和非拥有引用指针 和 单一所有者及其别名的智能指针 中介绍)
- 公共和私有作用域可见性(在此首次介绍)
它完全由两个智能指针构成,每个都有公共和私有变体。
- owner_ptr<T>:独占所有者。
- ref_ptr<T>:单一所有者的智能观察者。
我相信它是一个具有广泛应用前景且易于采用的实用解决方案。
单一所有者的智能观察者
标准库中有 auto_ptr<T>
和 unique_ptr<T>
这样的智能所有者,但没有对应的智能观察者。我们必须依赖原始指针来引用对象,而这些原始指针可能会悬空。确保所有曾被告知过对象信息的内容……在对象被删除时也能得到通知的任务,需要不合理的细致和极易出错。此外,未能正确处理这一点是 C++ 项目复杂性增加时,导致棘手问题的常见原因,而对它的恐惧可能会严重限制设计。
对于共享所有权,已经存在智能观察者的概念,即 std::shared_ptr<T>
(共享所有者)和 std::weak_ptr<T>
(观察者)。之前文章中介绍的owner_ptr<T> 和 ref_ptr<T> 是其单一所有权等价物,通过对独立的引用计数器进行相互访问来工作。然而,与 std::weak_ptr<T>
不同,ref_ptr<T> 是一个一流的智能指针,支持直接解引用 ->,使其非常方便使用。
这些智能指针的成功之处在于,在我工作中,它们避免了大型复杂项目因程序基础设施层面的神秘无效指针错误而停滞不前,而且这一切都没有牺牲单一所有权、确定性析构或 RAID。
在我工作的过程中,我发现了一个次要但非常重要的好处。一旦你为所有者(在此是 owner_ptr
)和观察者(在此是 ref_ptr
)提供了所有必要的转换,使它们能够协同工作并禁止它们之间无意义的操作,你就会获得一种新的类型敏感性,基于所有者和观察者之间的区别,编译器可以检查其正确使用。这使你的代码保持连贯和清晰。
公共和私有作用域可见性
我写这第三篇文章是因为我一直在宣扬这些智能指针的全面使用,但实际上发现许多情况表明,根本不需要引用计数的复杂性,使用它只会造成不必要的开销。于是我开始研究什么具体情况需要使用引用计数。答案很简单:
- 如果一个所有者在其声明的作用域之外是可见的,那么它必须与任何观察者一起被引用计数。我将这定义为公共作用域可见性。需要一个独立的引用计数器,因为不仅对象本身,所有者智能指针也可能在观察者仍然存在的情况下消失。
- 注意:应注意,动态集合的每个元素都存在于自己的作用域中,因此对它的任何引用都是公共的,并且必须进行引用计数,才能始终保证其安全性(当然,如果你只是要快速使用它而不干扰集合,也可以使用原始指针)。
- 如果一个所有者仅在其声明的作用域内可见,那么它就没有理由进行引用计数,观察者可以安全地采用对所有者智能指针的 const 引用形式。我将这定义为私有作用域可见性。由于它仅在其自身作用域内可见,只要有任何东西引用它,它就会一直存在。对象可能会消失,但智能指针始终在那里表明这一点。将引用限定为 const 使其成为一个真正的观察者,无法承担所有者的角色。
智能指针 owner_ptr<T> 和 ref_ptr<T>
在这里,我 lấy 之前的文章中的 owner_ptr
(单一所有者)和 ref_ptr
(观察者),去除了一些阻碍随意采用的过度安全特性,并以更轻量级的形式将其呈现为 Public
变体:
owner_ptr<T, Public> 和 ref_ptr<T, Public>
并提供零开销的
Private
变体:
owner_ptr<T, Private> 和 ref_ptr<T, Private>
- 也表示为默认:owner_ptr<T> 和 ref_ptr<T>
Public
(引用计数)变体可以在所有上下文中编译,但 Private
(零开销)变体如果被用于 Public
目的则无法编译。因此,逻辑上将它们设为默认 Private
,仅在情况需要时才设为 Public
(编译器会报错提示)。Private
变体对作用域可见性的这种敏感性是由 ref_ptr<T, Private>
的设计提供的,它是一个 const owner_ptr<T, Private>&
的包装器,这是一个 C++ 引用,必须在声明时初始化。由于其本质是 const
,它永远不会指向不存在的任何东西,而 owner_ptr<T, Private>
只能通过值声明(这是它的一个特性),所以它将始终与 ref_ptr<T, Private>
一起存在。
Public
和 Private
变体都遵循相同的基本交互语法,此处以 Private
变体为例。
owner_ptr<T> apT(new T); // apT owns the object and guarantees to delete // it.
if(apT) //is valid or tests as NULL
apT->DoSomething(); //behaves just like a pointer
apT=NULL; //deletes the object
//deletes the object when it goes out of scope
ref_ptr<T> rT( apT); //rT observes the same object while it lives
if(rT) //is valid or tests as NULL – knows when object has been deleted
rT->DoSomething(); //behaves just like a pointer
rT=NULL; //forgets about the object
//does not delete the object when it goes out of //scope
禁止 owner_ptr<T>
之间的赋值以及与之相关的危险的隐式所有权转移,从而强制执行正确使用。
owner_ptr<T> apT(new T);
owner_ptr<T> apT2( apT); //Compiler error two owners can never own the same object
以及任何尝试将 ref_prt<T>
指向尚未被 owner_ptr<T>
拥有的对象
ref_ptr<T> rT(new T); //Compiler error you cannot observe an object that has no owner
也同样被禁止。
所有权转移
所有权转移可以显式进行。
yield() 点方法将释放所有权,以便可以被另一个所有者消耗。
owner_ptr<T> apT(new T);
owner_ptr<T> apT2((apT.yield()); //Zeroes existing observing ref_ptr s
//They loose contact on transfer
//of ownership
swap(...) 点方法将交换两个相同类型的拥有者。
owner_ptr<T> apT(new T);
owner_ptr<T> apT2.swap(apT); //Zeroes existing observing ref_ptr s
//They loose contact on transfer
//of ownership
Public 变体
无论何时 ref_ptr
的作用域比它引用的 owner_ptr
更广,就需要 Public 变体。最简单的情况是:
ref_ptr<T, Public> rT;
{
owner_ptr<T, Public> apT(newT);
rT=apT; //this would not compile if either pointer were declared with
//Private scope visibility
rT->DoSomething();
}
if(rT) //Tests as NULL, apT has gone out of scope and object has been deleted
rT->DoSomething(); //not called
真实生活中的例子发生在当一个功能模块需要直接引用由另一个模块动态创建且可能不存在的东西时。最典型的就是动态数组的元素。
Public 变体有几个额外的特性:
-
对象的所有权可以在不将所有观察引用清零的情况下转移。
owner_ptr<T> apT(new T); owner_ptr<T> apT2( apT.yield_with_refs()); //Conserves existing observing ref_ptr s //They continue to reference the object until //the new owner destroys it. apT2.swap_with_refs( apT); //Conserves existing observing ref_ptr s //They continue to reference the object until //the new owner destroys it.
-
通过附加类 gives_ref_ptr<T> 提供对 '
gives_ref_ptr<T>this
' 指针的安全访问带有
public
方法:ref_ptr<T>
ref_ptr_to_this()与
shared_ptr/weak_ptr
等价物不同,ref_ptr_to_this()
是一个公共方法,并且不关心对象是如何被拥有的。这意味着当一个类定义在其继承列表中有gives_ref_ptr<T>
时,你可以按值声明类对象,仍然可以调用它们上面的ref_ptr_to_this()
来获取一个安全的ref_ptr<T, Public>
。class CClass : public gives_ref_ptr< CClass > { }; CClass object; ref_ptr< CClass >rObj= object. ref_ptr_to_this();
你应该考虑为什么需要这样做,因为大多数情况下使用 C++ 引用会更合适。
CClass object; CClass& Obj= object;
但是,初始化对任何动态创建的对象的值成员的引用是不安全的——这通常由指针解引用、返回值或初始化中的
[]
运算符表示。CParent { CClass object; }; vector< CParent > v; v.set_size(12); CClass& Obj=v[8].object; //unsafe
在这些情况下,获取一个
ref_ptr<T, Public>
是一个很好的解决方案。ref_ptr< CClass, Public >rObj= v[8].object. ref_ptr_to_this();
如果
v[8]
被移除或移动,那么rObj
将测试为零。并非总能修改类定义以继承
gives_ref_ptr<T>
,或者这样做是可取的。替代方法是使用基类模板 super_gives_ref_ptr<T> 在类对象声明中。
super_gives_ref_ptr<CClass> object; ref_ptr< CClass >rObj= object. ref_ptr_to_this();
创建临时副本的 owner_ptr 集合
owner_ptr
有第三种变体,专门设计用于存储在可能强制创建元素临时副本的 STL 集合中。你会发现 owner_ptr
的 Public
和 Private
变体在这种情况下都无法编译,因为它们禁止赋值不允许创建临时副本。每当发生这种情况时,都可以使用 owner_ptr<T, ElementType> 作为数组元素类型,它将编译并正常执行。它被引用计数并具有公共作用域可见性,并且可以被 ref_ptr<T, Public>
以与 owner_ptr<T, Public>
完全相同的方式引用。
std:vector<owner_ptr<T, ElementType> > v;
v.resize(5);
v[0]=new T;
ref_ptr<T, Public> rA=v[0];
它是一个混合智能指针,是 owner_ptr<T, Public>
的一个变体,允许在集合中创建临时副本。这种对“禁止赋值”规则的放宽确实会带来一些编码危险。
第一个很容易避免,并且陷入其中可能被描述为滥用。
owner_ptr<T, ElementType> apMyT; //Don't do this
ElementType
这个别名提醒我们,它只能用作集合的元素类型。将其用于命名变量将产生定义明确但意料之外的行为。
第二个是真正的危险,尤其是当你要将现有代码改造到这个系统中时。owner_ptr
集合元素之间的所有权转移应显式进行,例如:
v[1] = v[0].yield();//explicit transfer of ownership
但是你会发现这也编译通过……
v[1] = v[0]; //not what you will be expecting
它的效果是定义好的,不会导致内存泄漏或悬空指针,但它不是预期的,行为也不会如你所料。
隐式转换为原始指针
与之前的版本不同,此版本的 owner_ptr
/ref_ptr
支持隐式转换为原始指针。强制使用冗长的方法标记你获取了原始指针可能有用,但这意味着你必须对所有调用接受原始指针的库和 API 使用该冗长的方法,因为它们就是这样做的。此外,这些库和 API 很可能永远不会符合这个范式,所以为什么要用暗示它们应该这样做的冗长参数来惩罚它们呢?这是从使其“傻瓜式”的一个大让步,而我最初的目标是做到这一点——一个只会让不傻瓜的人沮丧的徒劳追求。现在这意味着你可以像这样轻易地“射穿自己的脚”:
owner_ptr<T> apT(new T);
T* pT=apT;
delete pT; //Whoops!! - runtime error
通过原始指针转换,你可以真正搞砸智能指针之间的关系。
owner_ptr<T> apT(new T);
owner_ptr<T> apT2(apT);//Compiler error - not allowed
//but
owner_ptr<T> apT2((T*)apT);//Whoops!! - two owners of the same object, double delete ahead
就是不要这样做!
优点是可以将这些智能指针简单地通过更改指针声明来应用到现有代码中。其他一切都可以保持原样。
从函数返回所有权
有一个 owner_return
类(Public
和 Private
变体),它不是一个智能指针,但它是一个所有者。在大多数情况下,这是一个隐藏的类,由 yield()
和 yield_with_refs()
点方法用于进行所有权转移。唯一显式的用法是作为你想要使用 yield()
或 yield_with_refs()
返回所有权的函数的返回类型。
owner_return<T> ReturnT()
{
owner_ptr<T> apT(new T);
//initialisation code
return apT.yield();
}
owner_ptr<T> apT2(ReturnT()); // ownership transferred to apT2. Object is deleted if nothing takes ownership
自定义删除器
你也可以在 owner_ptr
(Public
和 Private
)中将自定义删除器指定为第三个模板参数,如下所示:
struct my_deleter
{
template <class T>
static inline void Delete(T* p)
{
p->Release();//Your alternative object deletion method
}
};
owner_ptr<T, Public, my_deleter> apT;
GetObject((void**)&apT); //COM type initialisation works
//if apT is not already holding anything
侧边栏 - COM 类型初始化 &apT
实际上返回的是指向 owner_ptr<T>
的指针,而不是指向对象的指针。必须这样做,否则它会在某些 STL 容器中出错。COM 类型初始化工作正常,因为 owner_ptr<T>
的第一个类成员是指向对象的指针,因此具有相同的地址。由于 GetObject()
函数认为它是指针的地址,所以它只会填充到那里,而不会覆盖智能指针的更多内容。智能指针被欺骗了,但没关系,只要它还没有持有对象。
运行时开销
owner_ptr<T>
和 ref_ptr<T>
的 Private
变体没有任何内存开销(不比原始指针本身多),并且除了必须编写的代码之外,唯一的代码开销是在解引用时进行的测试,如果指针无效则抛出异常(这种定义和可处理的常见错误响应为我节省了很多麻烦)。
Public
变体的大小是原始指针的两倍,当一个对象被多个智能指针引用时,会创建一个引用计数器。最坏情况下的内存比例是原始指针的 2.5 倍,发生在第一次从 owner_ptr<T, Public>
获取 ref_ptr<T, Public>
时,导致第一次创建引用计数器。Public
变体的大多数操作都会执行一些代码,但没有一个是迭代或递归的,没有遍历数组或列表,并且引用计数器的调整不涉及任何线程同步。注意:在一般情况下,独占拥有的对象只能被拥有它的线程安全地引用。因为拥有线程可以立即销毁对象(这是单一所有者的特权),所以它是唯一可以确定对象仍然存在的线程。
如何利用这些智能指针
具有 Public
/Private
作用域可见性的 owner_ptr
/ref_ptr
提供了一种统一的方法,可以提供广泛的安全编码领域。这并不意味着你不应该做任何超出安全领域的事情,这就是 C++,但它确实意味着你可以常规地安全地做事。
将事物包装在 owner_ptr
/ref_ptr
中可以保证安全,而获取原始指针则会危及它们。你仍然会发现有些情况用原始指针处理得更好,但当它们出现时,你就能更仔细地关注它们。
Public
变体意味着你可以有许多组件相互持有引用并相互传递引用(就像人类世界一样),而不会变成一个噩梦,在删除某些东西时还要跟踪所有需要清零的指针。程序员使用带有垃圾回收器的语言来编写这类大部分代码,但内存使用会变得混乱、过于严谨,最终变得缓慢。
Private
变体具有零开销,而 ref_ptr<T, Private>
(一个 C++ 引用的包装器)的 const
特性确保它不会被意外地用于 Public
作用域上下文。
此解决方案是以下情况的替代方案:
- 由悬空指针引起的难以发现的错误
- 为避免悬空指针而需要过度的细致
- 由于害怕上述问题,设计、复杂性和规模受到限制
- 由于害怕上述问题,通过诉诸垃圾回收而丢失单一所有权、确定性析构和 RAID 的优点
- 由于害怕悬空指针而诉诸不当使用
std::shared_ptr
,导致丢失单一所有权的优点(实际上是一种容易内存泄漏的即时响应垃圾回收器)。
何时必须使用其他智能指针
当共享所有权是你的特定设计意图时,请使用 shared_ptr
和 weak_ptr
。
对于可能被多个线程同时访问的任何对象,请使用 shared_ptr
和 weak_ptr
。在这种情况下,你可能还需要保护你正在处理的对象免受同时访问。
它是如何工作的
owner_ptr<T, Private>
是一个非常直接的独占所有权指针,类似于 std::auto_ptr
、boost::scoped_ptr
或 std::unique_ptr
。它有一个特定的设计特性,即禁止隐式所有权转移,但提供显式所有权转移的机制。除此之外,可以使用上面提到的任何一个标准智能指针代替。
ref_ptr<T, Private>
仅仅是一个 const owner_ptr<T, Private>&
的包装器,一个指向拥有智能指针(而不是指针指向的对象)的 C++ 引用。它是唯一可以从 owner_ptr<T, Private>
获取的智能指针,其 const
性质使其无法拥有 Public
作用域可见性。
owner_ptr<T, Public>
和 ref_ptr<T, Public>
对的交互更复杂。它们的大小都是原始指针的两倍,包含指向对象的指针和一个指向独立引用计数器的指针(如果存在)。当 ref_ptr<T, Public>
被设置为指向 owner_ptr<T, Public>
时:
owner_ptr<T, Public> apT(new T);
ref_ptr<T, Public> rT( apT);
将创建一个独立引用计数器,它包含一个强计数和一个弱计数。其初始值为:
强计数 1 – 对象有效,有一个所有者。
弱计数 2 – 对象被 2 个智能指针引用。
同时,两个智能指针都将其指针设置为指向新创建的引用计数器。
如果进一步的 ref_ptr<t,>
ref_ptr<T, Public> rT2( rT);
或
ref_ptr<T, Public> rT2(apT);
那么弱计数将增加到 3。
- 如果任何一个
ref_ptr
被置零或重置,那么弱计数将减少 1。 - 如果
owner_ptr
被重置,那么强计数被设置为零,弱计数减少 1。 - 当你尝试测试或使用
ref_ptr<T, Public>
时,它会查看它指向的引用计数器中的强计数,如果为零,那么ref_ptr<T, Public>
将测试为 NULL 或无效。 - 当引用计数器中的弱计数下降到零时,引用计数器将自我销毁。
owner_ptr<T, ElementType>
的工作方式略有不同。它必须在集合中允许创建临时副本。为此,它允许拷贝构造和赋值,并将其解释为共享所有权(增加强计数),而析构或超出作用域则解释为释放所有权(减少强计数)。然而,每个元素都必须表现为单一所有者,而不是共享所有者,所以重置元素(设置为 NULL)会强制强计数为 0 并立即销毁对象。因此,它保留了确定性析构和及时的 RAID。
源代码
它只有一个头文件,除了 C++ 语言之外没有任何依赖。
它被封装在 namespace xnr{
中,所以你需要使用 namepace xnr;
或在所有内容前加上 xnr::
前缀。
继承仅保留给真实的“is a”关系;owner_ptr<T, ElementType>
是一个修改过的 owner_ptr<T, Public>
。除此之外,继承被避免了,仅仅是因为我发现智能指针的继承树在调试器中显示并迫使我向下钻取才能找到我想看的对象,这很烦人。实际上,你希望智能指针像语言元素一样工作,你不想让它们迫使你导航它们的内部。注意:Public 和 Private 变体之间没有继承关系,它们的结构不同。是部分模板特化允许它们可以方便地归类在相同的名称下。
如果设计倾向于继承,xng
类将是一个常见的基类。相反,它是一个 private static
方法的类,只能由被声明为友元的 Public
变体智能指针访问。它包含了 ref_counter
类的定义和一组松散类型的模板方法,定义了通用操作。严格的类型检查在调用它们的智能指针的定义中进行。
智能指针定义包含许多声明为 private
的转换,以禁止它们。其中一些是必需的,以防止使用不受欢迎的编译器默认设置,另一些是为了防止由隐式转换为原始指针所带来的不受欢迎的转换。
通过利用 xng
类的通用操作,我已经能够简洁地布置智能指针的定义,大多数方法都适合一行。这样可以更轻松地审查整体结构。我已尽力使其对任何想审查和验证它的人尽可能舒适。