65.9K
CodeProject 正在变化。 阅读更多。
Home

单一所有者智能指针及其别名

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (12投票s)

2012年11月13日

CPOL

29分钟阅读

viewsIcon

39706

downloadIcon

392

C++ 中完整内存和指针安全缺失的一环

owner_ptr<T> apT = new T;    //apT is a safe exclusive owner
ref_ptr<T> rT = apT;        //rT is a safe observer

vector<owner_ptr<T, in_collection> > vT;    //a safe exclusive owner collection
ref_ptr<T> rT = vT[i];            //safe observer of an element 

下载源代码 (xnr.zip) - 9.4KB - 3个文件 ( xnr_ptrs.h, xnr_lean_ptr_engine.h, win_refcount_pool.h)

[ 2012年11月22日 - 已修复一个导致无法编译的旧代码疏忽 ]

本文已被以下内容取代:

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

请使用新文章中更成熟的版本

背景

C++ 是一门优秀的语言,但自其诞生以来,一直存在一个导致诸多麻烦的缺陷。这个缺陷在于处理动态分配对象的语言元素缺乏适当的闭包。newdelete 关键字被引入以封装动态分配对象的创建和销毁,但没有添加新的语言元素来处理这些对象的引用。相反,这个角色由现有的 C 语言指针来承担,而这些指针是为指向 C 数组而设计的。

由此导致的麻烦表现为内存泄漏和悬空指针。指针的使用概念因此受到指责,Java 及其垃圾回收解决方案应运而生。随着 C# .NET 的出现,离开 C++ 转向垃圾回收解决方案的趋势进一步增强,其驱动力是对 C++ 及其行为不端的指针的恐惧。这种偏离在很大程度上是不恰当的,对所生产软件的设计和性能都产生了负面影响。

C++ 的问题不在于它使用指针,而在于它使用的指针定义和约束不足。例如,当在编译时非常清楚指针算术的使用只会导致灾难时,new 运算符返回的单个对象的指针能够进行指针算术,这不符合 C++ 的精神。

这里以一组智能指针的形式呈现了缺失的语言元素。它们不是一套根据判断和偏好使用的工具包,而是作为围绕 new 运算符形成闭包的语言元素,并且它们具有形式化的交互语法。它们作为更丰富的指针声明的使用,可以完全安全地管理和引用由“new”运算符返回的对象。

格式良好的 shared_ptr/weak_ptr 对

std::shared_ptr / weak_ptr 对在共享所有权的世界中为“new”运算符提供了完美的闭包,它们协同操作的方式启发了本文介绍的工作。

这两种智能指针类型具有明确的角色:shared_ptr 是所有者,通过持有对对象的引用使其保持活动状态。weak_ptr 是观察者,它的引用不会使对象保持活动状态,而是在对象销毁时自动归零。此外,还有一些交互语法:只有 shared_ptr 可以接收由“new”运算符返回的新对象,weak_ptr = new T 将无法编译。

shared_ptr / weak_ptr 对还具有支持跨线程共享引用的优点。它这样做是自然的,因为共享所有权的通常需求是提供对通用服务器类型资源的共享访问。这种线程安全要求影响了它们的设计,其具体结果是它们无法舒适或正确地采纳流行且有用的单一所有权范式。尽管单一所有权可以被视为共享所有权的特例,但严格的单一所有权与 shared_ptr 的线程安全要求是不兼容的。

拯救单一所有权设计

有一种流行的做法是,将 `shared_ptr` 用于所有引用,并将 `weak_ptr` 保留用于打破“循环引用”。这是一种不好的做法,而且它会产生毫无意义的循环所有权模式,这正说明了这一点。通常,代码的功能和设计并不真正需要任何所有权共享,但以前没有这样一套适用于单一所有权场景的智能指针。本文提供的正是这种功能。虽然与 `shared_ptr / weak_ptr` 有些相似之处,但共享所有权和单一所有权具有不同的特性。其中之一是单一所有权智能指针提供了更严格的交互语法,这极大地有助于保持代码的正确性、连贯性和可读性。

所有者和观察者

已经存在一系列自主的单一所有者智能指针,例如 `auto_ptr`、`scoped_ptr` 和 `unique_ptr`,它们可以保证自身的安全,但不能保证从它们获取的任何引用或别名的安全,这些引用或别名可能会悬空。缺失的功能是一个安全的观察者,相当于 `weak_ptr`,当其所指对象被销毁时会自动归零。由于 `shared_ptr` 和 `weak_ptr` 是设计成协同工作的,因此单一所有权也需要如此。这些在这里以以下形式提供:

owner_ptr<T> 对象的单一所有者,可以被 ref_ptr<T> 观察

ref_ptr<T> owner_ptr<T> 的观察者或别名,支持 -> 运算符

它们的工作方式如下

owner_ptr<T> apT=new T;        // apT owns the object and guarantees to deletes 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> 可以接收新创建对象的原始指针

只有 ref_ptr<T> 可以指向 owner_ptr<T>

只有 ref_ptr<T> 可以指向 ref_ptr<T>

最重要的是,owner_ptr<T> 不允许指向另一个 owner_ptr<T>,并且在两个 owner_ptr<T> 之间明确禁止暗示此操作的赋值运算符“=”。这不仅避免了将“=”解释为所有权转移(如在 auto_ptr<T> 中)的臭名昭著且反直觉的解释,而且也是强制执行连贯语法规则的关键。

所有权可以从一个 owner_ptr<T> 转移到另一个,但只能通过使用以下动词之一明确地进行:

adopt(owner_pr<T> opT) 所有权被转移,现有观察者或别名保持有效。这最接近原始指针的模型。

steal(owner_pr<T> opT) 所有权被转移,所有现有观察者或别名都重置为 null。对该对象的唯一剩余引用是其新所有者。

这些动词作为接收 `owner_pr` 的点方法以及全局函数提供,因此以下是等效的。
op2. adopt(op1);    //slightly more efficient
op2 = adopt(op1);    //sometimes more convenient    

所有者和观察者声明的语法能力

不符合这些规则的代码将无法编译。这种编译器强制的交互语法效果出乎意料的强大。代码通过所有者或观察者状态的清晰声明而变得清晰。即使您对所有者和观察者角色的理解有点模糊,它也会很快为您指明方向。

它的工作原理如下

使用“new”运算符时,必须声明一个 owner_ptr<T> 来接收其返回值。

owner_ptr<T> apT=new T;     

如果您尝试改用 `ref_ptr`,将会收到编译器错误。`ref_ptr` 不能拥有一个对象。

如果您想为这个对象取一个别名以在其他地方使用,则必须将其声明为 ref_ptr<T>

ref_ptr<T> rT =   apT;      

如果您尝试改用 `owner_ptr`,则会收到编译器错误。您不能对同一个对象拥有两个排他所有者。

你可以从一个别名中取另一个别名。

ref_ptr<T> r2T =   rT;   

但是,如果您将一个 owner_ptr<T> 指向一个别名 ref_ptr<T>,您将收到一个编译器错误。如果该对象存在,它已经有一个所有者。

在集合中保持单一所有权——“in_collection”修饰符

owner_ptr<T>ref_ptr<T> 都可以用作集合的元素,包括 STL 中的集合。

然而,`owner_ptr` 必须使用“in_collection”修饰符声明,如下所示。

vector< owner_ptr<T, in_collection> > v;    

如果将 `owner_ptr` 在执行任何类型元素复制的集合中(大多数集合都如此)使用而没有“`in_collection`”修饰符,您将收到编译器错误。对于单一所有者而言,作为集合元素的生活与作为命名变量的生活之间存在一些重要差异。“`in_collection`”修饰符考虑了这些差异,同时不损害单一所有权的确定性销毁保证。`owner_ptrin_collection>` 与 `ref_ptr` 的交互方式与普通的 `owner_ptr` 完全相同。

警告:不要使用 `=` 运算符在 `owner_ptr` 集合之间转移所有权,如下所示:

ArrayOfOwners1[i] = ArrayOfOwners2[i] ; // unexpected behaviour 

结果不是未定义的,也不会引起内存泄漏或悬空指针,但它不会是您所期望的,而且不太可能有用。无法安排编译器错误来阻止这种情况发生。

相反,请遵循编译器可以强制执行的规则,使用单独的命名所有者,并改用“`adopt`”和“`steal`”动词。

ArrayOfOwners1[i].adopt( ArrayOfOwners2[i]) ; //perfectly safe
//and
ArrayOfOwners1[i].steal( ArrayOfOwners2[i]) ; //perfectly safe

是完全安全的。

允许特殊情况下的臭名昭著的破坏性复制——“returnable”修饰符

将“=”解释为所有权转移的传统,也称为破坏性复制,并非没有道理。它提供了一种转移所有权的方式,并允许函数返回所有者。

`owner_ptr` 故意强制所有权转移通过 `adopt` 和 `steal` 动词明确且可见,但如果您需要从函数返回一个 `owner_ptr`,那么您将需要 `owner_ptr` 明确禁止的隐式“`=`”作为所有权转移的解释。

对于类工厂和对象初始化例程等特殊情况,当需要将所有权作为返回值传递出去时,`owner_ptr` 可以用“returnable”修饰符声明,如下例所示。

owner_ptr<T, returnable> NewObject()
{
    owner_ptr<T, returnable> pObj= new CObject;
    //object initialisation
    return  pObj;
}  

returnable”修饰符启用“=”作为所有权转移。当将返回值从函数传出时,破坏性复制没有负面影响,因为函数内部的一切都会在超出范围时被销毁。

owner_ptr<T, returnable> 具有稍微不同的交互语法

它允许使用 = 运算符隐式转移所有权

在两个 owner_ptr<T, returnable> 之间

以及从 owner_ptr<T, returnable> 到任何没有“returnable”修饰符的 owner_ptr<T>

它只能声明为局部变量。

“this”指针和对按值声明的对象的安全引用

ref_ptr<T> 是对已拥有对象的安全引用,但它可以以除 owner_ptr<T> 之外的方式拥有。对象可以按值声明,因此由函数或类的作用域拥有,或者您可能想从类内部使用“this”指针获取引用,在这种情况下,您可能不知道它是如何被拥有的。

提供了一个基类,可以添加到任何类的继承列表中
class gives_ref_ptr<T>
它会修改任何类,使其通过提供一个方法来安全访问“this”指针
ref_ptr<T> ref_ptr_to_this() 方法。
这类似于 std::shared_ptr/weak_ptr 库中的类 enable_shared_ptr_to_this<T>,但它返回一个 ref_ptr<T> 而不是 shared_ptr<T>,这是一个显著的区别。
继承自 `gives_ref_ptr` 的类的声明或所有权没有限制(除了它不能容易受到另一个线程操作的影响),并且公共方法 `ref_ptr_to_this()` 将始终为您提供一个安全的引用。

具体来说,这使您能够获取一个 ref_ptr<T>,指向按值声明的对象。

class CMyObject : public gives_ref_ptr<CMyObject>
{
 
};
 
MainFunc
{
    CMyObject Object;
    ref_ptr<CMyObject> rMyObject=MyObject.ref_ptr_to_this().
 
    //0r

    FunctionThatTakesRefPtr(MyObject.ref_ptr_to_this());
}   

这处理了您通常会使用取地址运算符 `&` 来获取指向按值声明的对象的指针,或者设计函数以接受 C++ 引用而不是这种情况。

与指针和 C++ 引用不同,ref_ptr_to_this() 在所有情况下都能提供完全安全的 ref_ptr<T>
在某些情况下,您可能希望获取按值声明的变量的安全引用,但您无法控制类定义。在这些情况下,您可以使用 referencable_value<T> 超类。

如果你有

CLibraryClass m_LibClass; 

并且您希望能够持有对 m_LibClassref_ptr,那么您可以将其声明为

referencable_value<CLibraryClass> m_LibClass;  

这将允许

ref_ptr<CLibraryClass> rLibClass= LibClass.ref_ptr_to_this(); 

这不会以任何其他方式改变 m_LibClass 的行为,因此追溯进行此调整没有问题。

快速完成工作——`ref_ptr` 的“fast”修饰符

在某个时候(除非对象由另一个线程控制),您知道在进行多次解引用之前,一次测试就足够了——或者您认为您知道。

所以你会想写

if(r)
{
    r->DoSomething();
    r->DoThis();
    r->DoThat();
}  

ref_ptr<T> 的设计目标是即使您决定省略测试,也不会出现未定义行为,因此它在每次解引用时都会进行自己的测试,以便在解引用失败时发出定义的响应。这当然违背了省略不必要的检查以快速完成工作的初衷。

为此,提供了 `ref_ptr` 的变体,即 `ref_ptrfast>`。它用于快速执行小段代码。它不是检查每次解引用,而是对对象加锁,然后在不进行任何进一步检查的情况下解引用。

ref_ptr<T, fast> fr=r;
if(fr)
{
    r->DoSomething();
    r->DoThis();
    r->DoThat();
}  

锁的效果取决于对象的所有权。如果是共享的,它只是共享并保持对象活动。如果是单一拥有的,则不可能。在单一拥有的指针在被快速指针锁定时被删除的情况下,响应是

在调试版本中:抛出异常以提醒程序员出现严重错误。

在发布版本中:通过错误地保持对象活动直到快速指针处理完毕来避免即将发生的灾难。

程序员每天都会做出一次测试,多次解引用的决定。`ref_ptr` 让你有机会声明这个决定并获得性能提升。它还确保如果你做错了,你会收到一个指示错误发生位置的异常,而不是未定义的行为。如果在开发过程中错误未能被检测到,发布版本将尽力通过修改所有权规则来掩盖它,从而冒破坏销毁序列而不是立即崩溃的风险。

应该理解的是,如果对象不由另一个线程控制,则代码行之间不可能删除对象。唯一的危险是与快速指针在同一代码块中的操作无意中且间接删除了对象。这种情况很少见,但可能发生。

拥有该对象的全局 `owner_ptr` 归零。

指向传入对象父级的反向指针用于访问其所有者并将其归零。

调用 PeekMessage 允许执行各种代码,这可能导致对象的销毁。

ref_ptr<T, fast> 具有特定于其角色的语法。

它只能在函数或代码块内声明为局部变量。

const 一样,它必须在构造时初始化为其值,之后其值不能更改。

一个 ref_ptr<T, fast> 可以指向任何其他类型的指针——所有人的目的地!

没有其他类型的指针可以指向 ref_ptr<T, fast> - 无物之源!

接受智能指针参数的函数

丰富的指针声明为将指针传递给函数带来了新的维度,这在使用原始指针时并不明显。参数定义不仅指定了所指对象的类型,还指定了指针的角色。正是参数定义中的角色决定了什么被传递给函数,即使作为参数呈现的指针具有不同的角色。

结果是我们可以声明一个函数来接受 ref_ptr<T>

bool CheckThisRef(ref_ptr<T> rT)
{
    return rT->CheckMe();
}  

然后传递给它一个 owner_ptr<T>

owner_ptr<T> opT=new T;
 
bool bRes=CheckThisRef(opT); 

owner_ptr<T> 不会传递到函数中,其所有权也不受影响。相反,会自动创建一个临时的 ref_ptr<T> 来指向它,并将其传递到函数中。

一个非常重要的考虑是需要为通用函数的指针参数使用通用角色声明。

如果函数用于任何情况下的通用用途,那么最佳选择是 ref_ptr<T, fast>。一个 ref_ptr<T, fast> 可以指向任何其他指针类型,包括 std::shared<T>std::weak_ptr<T>,甚至是一个原始指针。这意味着如果 ref_ptr<T, fast> 被用作函数的参数类型,那么该函数可以接收任何指针角色作为参数。它还将在函数内部提供最快的执行速度,并保持传入指针的安全保证。

只要函数不尝试删除它所指向的对象或尝试存储对它的引用以供以后使用,ref_ptr<T, fast> 就可以用作函数的参数。如果函数执行了其中任何一个操作,那么它就不是真正通用的,并且它不会与 ref_ptr<T, fast> 编译,它是一个与基础设施链接的函数,应该改用 ref_ptr<T> 作为参数。

接受原始指针参数的函数

如果您必须将 owner_ptr<T>ref_ptr<T> 传递给接受原始指针参数的 API 函数,它不会自动转换为原始指针。您必须使用其 get_pointer() 点方法。

void ApiFunc(T* pT);
 
owner_ptr<T> apT= new T;
 
ApiFunc(apT); //will not compile

ApiFunc(apT.get_pointer()); //ok  

`get_pointer()` 方法有意地冗长,因为直接访问原始指针会危及指针系统的完整性。它使潜在的有害完整性破坏可见。

通过引用传递智能指针

这些智能指针可以通过引用传递给函数,并将产生与通过引用传递其他任何东西相同的效果、优点和限制。通过引用传递保证您正在处理原始传递的智能指针,而不是它的副本或临时引用。它还保证不会执行任何复制操作,因此无需对引用计数对象进行任何调整。当通过调用堆栈传递 `ref_ptr` 时,这可以节省大量开销,但链的开头必须有一个函数通过值接收 `ref_ptr`,以便通过从它所代表的原始生命周期更长的 `owner_ptr` 或 `ref_ptr` 转换来创建它。
传递引用时最重要的后果是对 owner_ptr<T> 的影响。

例如:

void DoStuffToThisOwner(owner_ptr<T>&  rT) //owner passed by reference
{
     rT=NULL;    
}  

然后传递给它一个 owner_ptr<T>

owner_ptr<T> opT=new T;
 
bool bRes=DoStuffToThisOwner(opT);  

在这种情况下,`DoStuffToThisOwner` 接收对原始 `owner_ptr` 的引用,并能够重置它。在某些情况下,这可能是您想要的,但大多数时候您不想要,并且

void DontDoStuffToThisOwner(ref_ptr<T>  rT) //must be by value to carry out conversion 

更有意义。

通过引用传递可能会发生许多其他意想不到的后果,但大多数情况下它们都会导致编译器错误,从而可以纠正问题。

作为一般规则:在长调用链中通过引用传递智能指针,但通过值传递开始这些链。

强制转换智能指针

这些智能指针隐式执行从派生类到基类的强制转换。

class T
{
 <span class="Apple-tab-span" style="white-space: pre;">    </span>//.......
};
class U :public T
{
 <span class="Apple-tab-span" style="white-space: pre;">    </span>//.......
}
 
owner_ptr<U> apU=new U;
ref_ptr<T> rT = apU;    //ok, implicit cast 

以及初始化时

owner_ptr<T> apT=new U;  //ok  

切勿在“new”的返回值和对 owner_ptr 的赋值之间放置强制转换。

owner_ptr<T> apT=(T*)new U; //NEVER do this   

它会阻止 owner_ptr 知道原始对象的大小。

与原始指针一样,它不会隐式地从基类转换为派生类,您必须提供显式转换,因为只有您知道这样做是适当和正确的。为此,您必须使用 ptr_cast<U>(any_ptr<T>) 函数。

owner_ptr<T> apT = new U;
 
ref_ptr<U> rU = ptr_cast<U>(apT)    

空指针 – NULL 或 null_ptr

NULL,一个数值零,并不是空指针的最佳表示。它可能被误认为是整数值参数。为此,提供了一个类型安全的 null_ptr,它不是一个数字。您可以使用 NULLnull_ptr,并且可以混合使用它们,但建议使用 null_ptr

指针系统概述


单一所有者

owner_ptr<T>

集合的变体

owner_ptr<T, in_collection>

类工厂的变体

owner_ptr<T, returnable>

所有权转移的特殊动词

adopt(owner_ptr<T>)

现有观察者保持有效

steal(owner_ptr<T>)

将所有现有引用归零

观察者

ref_ptr<T>

快速执行变体

ref_ptr<T, fast>

对象的附加基类

class gives_ref_ptr<T>

提供 ref_ptr<T> ref_ptr_to_this()

父类

referencable_value<T> var;

提供 ref_ptr<T> var.ref_ptr_to_this()

全部

any_ptr<T>.get_pointer() 访问内部指针——有意地冗长

ptr_cast<T>(any_ptr<U>) 显式转换为派生类

null_ptr NULL 的替代品,具有更高的完整性



与 std::shared_ptr 和 std::weak_ptr 的交互

这些新的智能指针与 std::shared_ptrweak_ptr 共存并互补,并在存在正确且可支持的解释时提供转换。

一个 ref_ptr<T, fast> 可以指向一个 shared_ptr<T>

一个 ref_ptr<T, fast> 可以指向一个 weak_ptr<T>

一个 shared_ptr<T> 可以 steal

一个 owner_ptr<T>

一个 owner_ptr<T, in_collection>

以及一个 owner_ptr<T, returnable>

“C”风格数组

此处介绍的智能指针围绕“new”运算符(如“new T”)提供闭包,它们绝不应与放置 new(如 new T[array_size])一起使用。虽然可以设计用于放置 new 的智能指针,但为了安全,它需要增加边界检查开销。由于放置 new 通常用于避免开销,因此这可能是一个毫无意义的练习。

如果您需要数组,那么通常使用集合类,例如 std::vector<T>CAtlArray<T>。它更方便、更易读、更安全。

如果您确实需要使用 `new T[array_size]` 的“C”风格数组的原始性能或简洁性。那么请使用原始指针,但将其封装并将其作为单个对象呈现给代码的其余部分。

C++ 的作者 Bjarne Stroustrop 对“C”风格数组有一些评论 http://www2.research.att.com/~bs/bs_faq2.html#arrays

您需要什么才能使用这些智能指针?

您开始使用这些智能指针所需的一切就是这个文件

xnr_ptrs.h

您可以通过以下方式使用它
using namespace xnr;
 
owner_ptr<MyClass> apMyClass = new MyClass;      // apMyClass is now safe
ref_ptr<MyClass> rMyClass = apMyClass        //rMyClass is also safe 

或者在集合的情况下

AtlArray<owner_ptr<MyClass, in_collection> > MyClassArray;;
MyClassArray.Add(new MyClass);
ref_ptr<MyClass> rMyClass =  MyClass[0];  

一旦您开始使用它,您可能会考虑使用引用计数对象池来增强它,这将防止残留的引用计数对象导致内存碎片化。这在 MS Windows 平台上可用,只需包含以下文件:

win_refcount_pool.h //修改以适应其他操作系统并不困难

在 xnr_ptrs.h 之上

并将宏 XNR_REFCOUNT_POOL_INSTANCE 放在全局作用域,并在这些包含之后,仅在您的一个 .cpp 文件中。


经验更丰富的程序员可能会考虑在没有该系统提供的删除完整对象的昂贵保证的情况下工作,而是确保所有用于所有权的基类的析构函数都标记为虚拟。这将减少智能指针使用的内存,尤其是在多态集合中。为此,请放置

#define XNR_LEAN_PTR_ENGINE

在 xnr_ptrs.h 之上


如果您正在使用的系统不支持 C++ 异常或 `exit(EXIT_FAILURE)`,则需要覆盖内置的异常处理。为此,请在 xnr_pntrs.h 上方包含一个包含以下内容的文件:

namespace xnr{
namespace eng{
 
void xnr_fatal_error(char * csReason)        //it is over – stop the program
{
    //Your choice of what action to take
}
void xnr_exception_error(char * csReason)    //catch and recovery may be feasable
{
    //Your choice of what action to take
}
 
} //namespace eng
} //namespace xnr

#define XNR_ERRORHANDLER_DEFINED    

它们会被滥用吗?

智能指针永远不是傻瓜式操作。它们旨在提供一种自动保护措施,以替代需要不合理勤勉才能正确编写和维护的代码。

从好的方面看,一旦您将一个新对象直接分配给一个 owner_ptr<T>,该对象将永远不会泄漏,并且您可以从中获取的所有引用都保证是有效或为空。唯一会破坏这种安全性的情况是:

没有正确地将新对象直接分配给 owner_ptr<T>

T* pT = new T;        //pT can be used to delete the object independantly
owner_prt<T> apT = pT;    //apT may not be safe   
owner_prt<T> apT =(T*)new U;    // apT never sees the original class U  
通过 get_pointer() 方法暴露原始指针。
owner_prt<T> apT = new T;    //safe so far
T* pT =  apT.get_pointer();    //pT can be now used to delete the object independantly 
您可能需要使用 get_pointer() 方法将对象传递给遗留或 API 函数。当您这样做时,您应该确保该函数不会删除对象(这会破坏您的代码),也不会存储它以供以后使用(这可能会破坏其代码)。冗长的 get_pointer() 方法证明您已将智能指针暴露于此潜在风险。

大多数其他滥用行为将被编译器检测并报告为错误,但有些编译器无法检测到。这些都不会违反内存或指针安全,并且它们的效果是明确的,但可能不是程序员所期望的。在每种情况下,程序员的意图都值得怀疑。它们是:

vector<owner_ptr<T,  returnable> > //This will compile with disastrous results, like auto_ptr 
owner_ptr<T, in_collection> apT = new T; // apT is not in a collection, it is a named variable 
vector<owner_ptr<T,  in_collection> > Array1;
vector<owner_ptr<T,  in_collection> > Array2;
// Fill Array1...
Array2[i] = Array1[i];    //This will not transfer ownership, it is closer to sharing it. 
最后一个不幸地允许并破坏了一种流行的(尽管现在已弃用)编程范式,其示例可以在现有代码中找到。

工作原理

这是一个引用计数智能指针系统。每个智能指针在创建时都持有两个指针值。一个指向对象的指针,最初为空,以及一个指向引用计数对象的指针,最初也为空,因为尚不存在引用计数对象。

引用计数对象及其行为因所使用的指针引擎而异。

默认(或安全)指针引擎的情况下:

引用计数对象包括

一个强计数——所有者的计数

一个弱计数 - 观察者的计数

指向原始对象的指针——确保其完全销毁

一个虚函数——允许根据其类型对原始对象进行操作。

它们被创建

第一次取得别名时

owner_ptr<T> apT= new T;      //still no reference counting object
ref_ptr<T> rT =  apT         //reference counting object created  

以及任何所有者隐式转换为基类时

owner_ptr<T> apT= new U;    //reference counting object created 

精简指针引擎的情况下

引用计数对象包括

一个强计数 ——所有者的计数

一个弱计数 - 观察者的计数

它们被创建

第一次取得别名时

owner_ptr<T> apT= new T;      //still no reference counting object 
ref_ptr<T> rT =  apT         //reference counting object created   

精简指针引擎具有较小的引用计数对象,并且创建它们的情况较少,但它不保证销毁完整的对象。因此,如果您使用它,您必须勤奋并正确使用虚析构函数。

在这两种情况下,当强计数降为零时,所指向的对象被销毁;当弱计数降为零时,引用计数对象销毁自身。

指针引擎定义了基类层次结构

_has_ref_counts – 任何与引用计数对象关联的基类

数据成员: reference_controller* m_pRC; //指向引用计数对象的指针

用于创建和操作引用计数对象的方法

还包含引用计数对象及其方法的定义

_ptr : protected _has_ref_counts – 所有智能指针的基类

额外数据成员: T* m_pT; //指向对象的指针

提供一组动词般的原始方法,最终的智能指针定义可以从中选择并适当使用。

_value_with_counts : protected _has_ref_counts – 为 gives_ref_ptr 公共附加基类提供受保护方法的基类。

没有额外的数据成员

默认(安全)指针引擎在 xnr_ptrs.h 中,精简指针引擎xnr_lean_ptr_engine.h 中。

xnr_ptrs.h 中其余代码与所选指针引擎无关。

首先,在智能指针定义中重复出现但略有变化的精心设计的构造函数和赋值代码模式被封装在参数化宏中。这使得最终的智能指针定义可以简化为更可读的允许转换和动词列表,这些转换和动词在它们出现时执行。

最后,智能指针本身使用参数化宏进行定义。

大多数代码都符合您逻辑上的预期,但也有一些不寻常的转折。


尽管该系统专注于单一所有权,但使用的是整数强计数,而不是简单地用布尔值来表示对象是否存在。这没有损失,因为布尔值占据的空间与整数一样多,它之所以存在是因为它允许

通过将强计数变为负数来由 ref_ptr<T. fast> 进行锁定

owner_ptr<T, in_collection> 拥有内部强计数,使其能够在集合中被复制后仍然存活。


in_collection 修饰符需要一些解释。许多集合类会对其元素进行临时内部复制,这会调用 = 运算符。这被正常的 owner_ptr<T> 禁止,并会导致编译器错误。支持 = 运算符并将其解释为破坏性复制的单一所有权指针(例如 auto_ptr<T>)会编译,但会产生灾难性结果。元素第一次复制时,所有权会转移给副本,当副本被销毁时,对象也会被删除。因此,人们普遍认为,只有 std::shared_ptr<T> 等共享所有权指针才能保存在此类集合中。问题在于,一旦使用集合,您就被迫进入共享所有权模型——这没有任何逻辑可言。

in_collection 修饰符执行两项操作来修改 owner_ptr<T>

提供了 = 运算符,并将其解释为共享所有权,它增加了强计数。

在销毁时,它不会直接销毁对象,而是减少强计数,并且只有当计数达到零时才销毁对象。

这种修改赋予了它共享所有权的特性,使其能够在集合中存活,但它保留了确定性销毁,因为任何元素的归零或重置都会立即将强计数设置为零(无论其先前值如何)并删除对象。对该对象的所有其他引用都会有效归零。

owner_prt<T, in_collection > 存在一个漏洞。提供了 = 运算符,以便集合可以执行其内部复制,但没有任何东西可以阻止它在两个集合之间使用,从而产生意想不到的结果,如下所示:

Array1[i] = Array2[i];   //does not have intended effect   

这将导致对象被两个数组引用,但其立即销毁将发生在在任一数组中归零时或从两个数组中移除时。在某些情况下,这可能是期望的行为,但这并不是大多数人所期望的。最好禁止任何看起来使两个所有者指针在可见代码中相等的操作,但在这种情况下是不可能的。对于命名的 owner_ptr<T>,编译器禁止这种结构。在 owner_ptr<T, in_collection> 的情况下,程序员必须承担避免它的纪律。


ref_ptr<T, fast> 包含一个额外的数据成员,一个指向函数的指针,允许立即执行所选的解引用机制,无需测试任何条件。该函数指针还用于标记 ref_ptr<T, fast> 运行的条件。因此,提供了四种方法,函数指针可以指向它们,尽管其中三种执行完全相同的操作。这仅仅是为了允许标记四种状态,尽管只需要两种机制。这两种机制是:

如果创建时有效且非零,则立即解引用

如果创建时为零,则抛出异常

解引用时不会评估任何条件,函数指针在创建时根据锁定机制进行设置。


_ptr 类的以下原始方法需要解释:

_reset() //递减强计数,如果为零,则删除对象

_quiet_reset() //递减强计数,永不删除对象

_hard_reset() //将强计数设置为零并删除对象

_quiet_hard_reset() //将强计数设置为零,永不删除对象

quiet”表示不删除对象

hard”表示将强计数直接设置为零


使用引用计数对象池的必要性需要解释。

您正在查看这个智能指针系统,可能已经玩过 `std:shared_ptr/weak_ptr` 对,因为您希望系统地避免访问无效指针的可能性。这是因为确保在删除对象时,同时将所有别名指针设置为 null 是一项费力且不确定的任务,会导致代码混乱,并且很容易因添加任何新别名而中断。

您可能只是想确保别名归零代码中的任何遗漏都不会导致问题,或者您可能想采取逻辑步骤,完全不编写该代码,而是让一切自动完成。无论哪种方式,您都预料到代码中别名的显式归零可能不完整。

现在让我们看看不显式归零别名会带来什么后果

使用未受保护的原始指针

无效引用可能导致未定义和灾难性的结果。

使用垃圾收集器或用 std::shared_ptr 模拟它

被引用的对象将保留在内存中,指针将提供对有效数据或有效代码的访问,尽管这可能不是程序员的意图。

使用 shared_ptr<T> 作为所有者,weak_ptr<T> 作为观察者,或者这里介绍的单一所有权智能指针系统

一个小的引用计数对象将保留在内存中,以向所有别名指示对象不再存在。它将一直保留,直到所有别名都被显式归零、测试并发现为空,或者已经超出作用域。

总结一下:别名不完全显式归零的效果是

原始指针——不可预测的崩溃

垃圾收集器——大型对象无意中保留在内存中,导致意外访问。

所有者和观察者智能指针——小型引用计数对象保留在内存中,不可能发生意外访问。

引用计数对象很小,但它们会一直存在,直到对它们的引用全部消失,就像垃圾回收器处理更大的对象一样。即使您完全放弃显式清零别名,导致大量引用计数对象的持久化,使用的总内存量也不是问题。问题在于,如果引用计数对象都是在堆上使用“new”运算符创建的,那么它们往往会分散在内存中与它们关联的对象之间。当这些对象被删除时,引用计数对象将保留下来,使已释放的内存碎片化

现在,许多 shared_ptr 代码都是在不担心这个问题的情况下编写的——内存既便宜又充足,但是,许多 C++ 代码都结构良好,允许返回干净的内存块,所以不要用我们的智能指针系统来破坏它!

引用计数对象池以温和的指数增长率在连续块中预分配所有引用计数对象的空间。这确保它们集中存储在内存块中,而不是分散各处,从而最大程度地减少碎片化。从池中分配也比从堆中分配每个对象更快。当池被激活时,引用计数对象的“new”和“delete运算符被重载,使它们直接与预分配的池而不是与堆一起工作。


建议

多年来,它一直支持一些复杂且不断增长的代码,并且运行良好。这并不意味着已经测试了所有可能的极端情况,但如果您能找到一个错误,我相信我们可以修复它或将其定义为其范围的限制。

我个人不愿意在没有它的情况下工作。请尝试使用它并用它进行构建!

座右铭

功能性且整洁的开销是良好的工程。糟糕的工程是允许混乱发生。

历史

这项工作是之前由同一作者在 TheCode Project 上发布的 XONOR 指针:独占所有权和非拥有引用指针 的衍生和取代。

© . All rights reserved.