c++11 条款20:使用std::weak_ptr作为一个类似std::share_ptr但却能悬浮的指针

来源:互联网 发布:超人计划软件 编辑:程序博客网 时间:2024/05/22 19:13

条款20:使用std::weak_ptr作为一个类似std::share_ptr但却能悬浮的指针

有一个矛盾,一个灵巧指针可以像std::shared_ptr (见条款 19)一样方便,但又不参与管理被指对象的所有权。换句话说,需要一个像std::shared_ptr但又不影响对象引用计数的指针。这类指针会有一个std::shared_ptr没有的问题:被指的对象有可能已经被销毁。一个良好的灵巧指针应该能处理这种情况,通过跟踪什么时候指针会悬浮,比如在被指对象不复存在的时候。这正是std::weak_ptr这类型灵巧指针所能做到的。

你可能疑惑std::weak_ptr能有什么用处,在你看了std::weak_ptr的API后可能更疑惑。它看上去根本不灵巧。std::weak_ptr不能解引用,也不能检查是否为空。这是因为std::weak_ptr不能作为一个独立的灵巧指针,它是作为std::shared_ptr的延伸。

指针生成的时刻就决定了这种关系。std::weak_ptr一般是通过std::shared_ptr来构造的。当std::shared_ptr初始化std::weak_ptr时,std::weak_ptr就指向了相同的地方,但它不改变所指对象的引用计数。


auto spw =                                   // after spw is constructed,
    std::make_shared<Widget>(); // the pointed-to Widget's

                                                    // ref count (RC) is 1. (See
                                                    // Item 21 for info on
                                                    // std::make_shared.)


std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget
                                                        // as spw. RC remains 1


spw = nullptr;  // RC goes to 0, and the
                       // Widget is destroyed.
                       // wpw now dangles

std::weak_ptr成为悬浮指针也被称作过期。你可以直接检测,


if (wpw.expired()) … // if wpw doesn't point
                                 // to an object…


但是经常期望的是检查一个std::weak_ptr是否已经过期,以及是否不能访问访问做指向的对象。这个比较难做到。因为std::weak_ptr缺乏解引用操作,没法写这样的代码。即使有,把检查和解引用分隔开来也会引起竞争冲突:在调用过期操作(expired)和解引用之间。另一个线程会重新分配或者删除指向对象的最后一个std::shared_ptr,这会引起的对象被销毁,于是你的解引用会产生未定义行为。


你所需要的是一个原子操作来检查std::weak_ptr是否过期,如果没过期则提供对所指对象的访问。可以通过从std::weak_ptr构造std::shared_ptr来实现上述操作。这个操作有两个形式,取决于假如你从std::weak_ptr来构造std::shared_ptr时std::weak_ptr已经失效你期望发生什么情况。一种形式是std::weak_ptr::lock,它返回一个std::shared_ptr。如果std::weak_ptr失效,则std::shared_ptr为空:


std::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw's expired,
                                                                        // spw1 is null
auto spw2 = wpw.lock(); // same as above, 
                                       // but uses auto


另一种形式是把std::weak_ptr作为参数来构造std::shared_ptr。这样,如果std::weak_ptr失效的话,则会抛异常:

std::shared_ptr<Widget> spw3(wpw); // if wpw's expired,
                                                            // throw std::bad_weak_ptr


可能你还是很疑惑std::weak_ptr怎样使用呢。设想一个工厂函数,基于唯一ID来创建一些指向只读对象的灵巧指针。根据条款18对工厂函数返回类型的建议,应该返回一个 std::unique_ptr:

std::unique_ptr<const Widget> loadWidget(WidgetID id);

假如loadWidget是一个昂贵的调用(比如因为涉及到文件或数据库io)而且经常会被相同的ID重复调用,一个合理的优化是写一个函数做loadWidget的工作,并且缓存结果。然而保持每一个请求过的Widget在缓存中可能会引起性能问题,所以另一个优化就是在Widget不再使用时删除之。

对这个工厂函数来说,返回一个std::unique_ptr并不是最合适的。调用者获得灵巧指针并缓存下来,调用者决定了这些对象的生存期,但是缓存也需要一个指向这些对象的指针。缓存的指针需要能够检测什么时候是悬浮的,因为工厂函数的使用者不在使用这些指针时,对象会被销毁,这样相关的cache项就会变成悬浮指针。于是缓存的指针应该是std::weak_ptr---这样可以检测到什么时候悬浮。那么这意味着工厂的返回值应该是std::shared_ptr,因为只有对象的生存期由std::shared_ptr管理时,std::weak_ptr才可以检测到何时悬浮。

这里有个快速但不好的loadWidget缓存实现方案:
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID,
                                          std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock(); // objPtr is std::shared_ptr
                                                // to cached object (or null
                                                // if object's not in cache)

if (!objPtr) {                              // if not in cache,
    objPtr = loadWidget(id);      // load it
    cache[id] = objPtr;               // cache it
}

return objPtr;
}

这个实现使用了c++11中的hash table容器(std::unorderer_map),尽管没有显示出WidgetID的hash计算和比较相等的函数。

fastLoadWidget 的实现忽略了缓存中的过期的std::weak_ptr会不断积累,因为相关联的Widget可能不再使用(因此会被销毁)。这个实现可以被改善,而不是花时间去深入到std::weak_ptr中去观察,让我们考虑第二种情况:观察者模式。改模式的主要部件是主题(Subjects,状态可以改变的对象)和观察者(Observers,状态改变发生后被通知的对象)。多数实现中,每个subject包含了一个数据成员,保持着指向observer的指针,这样很容易在subject发生状态改变时发通知。subject没有兴趣控制它们的observer的生命周期(不关心何时它们被销毁),但他要确认一个observer是否被销毁,这样避免去访问。一个合理的设计就是每个subject保存一个容器,容器里放着每个observer的std::weak_ptr,这样在subject在使用前就可以检查observer指针是否是悬浮的。

最后再举一个使用std::weak_ptr的例子,考虑一个数据结构,里面包含了A,B,C3个对象,A和C共享B的所有权,因此保持了一个B的std::shared_ptr:


假设有一个指针从B回指向A,那么这个指针应该用什么类型的指针呢?



有三中选择:

1.一个原始指针。 这种情况下,如果A被销毁,C依然指向B,B保存着指向A的指针,但是已经是悬浮指针了。B却检测不出来,所以B有可能去解引用这个悬浮指针,结果就是为定义的行为。

2.一个std::shared_ptr。这种情况下,A和B互相保存着一个std::shared_ptr,结果这个环路(A指向B,B指向A)组织了A和B被销毁。即使程序的其他数据已经不再访问A和B,它们两者都互相保存着对方一个引用计数。这样,A和B就内存泄漏了,实用中,程序将不可能访问到A和B,而它们的资源也将不会被重新使用。

3.一个std::weak_ptr。这将避免上述两个问题。假如A被销毁,那么B的回指指针将会悬浮,但是B可以检测到。进一步说,A和B虽然都互相指想彼此,但是B的指针不影响A的引用计数,所以当std::shared_ptr不再指向A时,并不能阻止A被销毁。

使用std::weak_ptr无疑是最好的选择。然而用std::weak_ptr来打破std::shared_ptr引起的循环并不那么常见,所以这个方法也不值一提。严格来讲层级数据结构,比如tree,孩子结点一般都只被父节点拥有,当父节点被销毁后,所有的孩子结点也都应该被销毁。这样,从父节点到子节点的链接可以用std::unique_ptr来表示,而反过来从子节点到父节点的指针可以用原始指针来实现,因为子节点的生命周期不会比父节点的更长,所以不会出现子节点去解引用一个父节点的悬浮指针的情况。

当然并非所有的基于指针的数据结构都是严格的层级关系的。比如像缓存的情况以及观察者列表的实现,使用std::weak_ptr就非常好。

从效率的角度来看,std::weak_ptr和std::shared_ptr几乎一致。它们尺寸相同,都使用了控制块(见条款19),其构造,析构,赋值都涉及了对引用计数的原子操作。你可能会吃惊,因为我在本条款开始提到了std::weak_ptr不参与引用计数的操作。其实那不是我写的,我写的是std::weak_ptr不涉及对象的共享所有权,因此不影响对象的引用计数。实际山控制块里面有第二个引用计数,std::weak_ptr操作的就是这第二个引用计数。更详细的描述见条款21。

                                                                需要记住的事情

1.使用std::weak_ptr来指向可能悬浮的std::shared_ptr一样的指针。

2.可能使用std::weak_ptr的情况包括缓存,观察模式中的观察者列表,以及防止std::shared_ptr环路。







0 0
原创粉丝点击