1

当一个对象需要引用另一个对象而不“拥有它”(即,不对其生命周期负责)时,一种方法是简单地为此使用原始指针或原始引用,如下例所示:

class Node
{
    std::vector<Edge*> incidentEdges;
};

class Edge
{
    Node* startNode;
    Node* endNode;
};

class Graph
{
    std::vector<std::unique_ptr<Node*>> nodes;
    std::vector<std::unique_ptr<Edge*>> edges;
};

(请节省时间来评论是否存在更有效的图形数据结构,这是我的专业领域,而不是问题的重点。)

Graph负责节点和边的生命周期,并负责保证其中的指针NodeEdge悬空。但是如果程序员没有这样做,那么就有未定义行为的风险。

但是由于引用计数的开销成本,人们可以强烈地强制使用智能指针不会发生未定义的行为。相反,它会优雅地崩溃。它保证这发生在尽可能早的时间(避免损坏更多数据)并且不会被忽视。这是一种可能的实现:

(编辑:固定实现,Yakk回答中的更多细节。非常感谢!)

template <class T>
using owning_ptr = std::shared_ptr<T>;

template <class T>
class nonowning_ptr
{
    std::weak_ptr p_;

public:
    nonowning_ptr() : p_() {}
    nonowning_ptr(const nonowning_ptr & p) : p_(p.p_) {}
    nonowning_ptr(const owning_ptr<T> & p) : p_(p) {}

    // checked dereferencing
    owning_ptr<T> get() const
    { 
        if (auto sp = p_.lock())
        {
            return sp.get();
        }
        else
        {
            logUsefulInfo();
            saveRecoverableUserData();
            nicelyInformUserAboutError();
            abort(); // or throw exception
        }
    }

    T & operator*() const = delete; // cannot be made safe
    owning_ptr<T> operator->() const { return get(); }

    // [...] other methods forwarding weak_ptr functionality 
};

class Node
{
    std::vector<nonowning_ptr<Edge>> incidentEdges;
};

class Edge
{
    nonowning_ptr<Node> startNode;
    nonowning_ptr<Node> endNode;
};

class Graph
{
    std::vector<owning_ptr<Node>>> nodes;
    std::vector<owning_ptr<Edge>>> edges;
};

我的问题是:除了明显的性能与安全性权衡之外,每种方法的优缺点是什么?

我不是在问哪个是最好的,肯定没有最好的,这取决于用例。我在询问您可能知道而我不知道的每种方法的事实利弊,这将有助于做出设计决策(也许,在可读性方面?可维护性?可移植性?与第三方库玩得很好?防止 use-after-free 漏洞利用?)。

4

2 回答 2

6

我的问题是:除了明显的性能与安全性权衡之外,每种方法的优缺点是什么?

忽略智能指针除了性能和安全性之外没有其他问题的事实(性能是我们不让 GC 安全处理它的原因),事实是你的nonowning_ptr类被严重破坏了。

您的get函数返回一个裸指针。然而,您的代码中的任何地方都不能保证任何用户get都会获得有效的指针或NULL.

在您销毁shared_ptr返回的weak_ptr::lock那一刻,您删除了唯一保持该内存有效的东西。这意味着,如果有人出现并删除了shared_ptr该内存的最后一个,而您拥有您的T*,您就完蛋了。

穿线尤其打破了你对安全的幻想。

所以最重要的“骗局”nonowning_ptr是它坏了;它并不比 a 更安全T*

于 2016-03-11T20:50:49.723 回答
4

您的设计有一个问题,如果另一个线程或执行路径(例如,函数调用的多个参数)修改了shared_ptr底层 your weak_ptr,将进行生命周期检查,并且在您使用它之前,您会得到 UB。

为了减少这种情况,T * get()应该是std::shared_ptr<T> get()。并且operator->也应该返回std::shared_ptr<T>->虽然这看起来不切实际,但它实际上是有效的,因为在 C++ 中定义了自动递归的有趣方式。(a->被定义为(*a).ifa是一个指针类型,(a.operator->())->否则。所以你->返回 a shared_ptr,然后->调用它,然后返回指针。这确保你正在执行的指针的生命周期->足够长。)

// checked dereferencing
std::shared_ptr<T> get() const
{ 
  if (auto sp = lock())
    return sp;
  fail();
}

void fail() { abort() } // or whatever
T & operator*() const = delete; // cannot be made safe
std::shared_ptr<T> operator->() const { return get(); } // works, magically

operator std::shared_ptr<T>() const { return lock(); }
std::shared_ptr<T> lock() const { return p_.lock(); }

现在p->foo();是(有效)p->get()->foo()。返回值的生命周期get() shared_ptr比对 的调用要长foo(),所以一切都像房子一样安全。

调用中仍然存在一个漏洞,T& operator()引用可能比其拥有的对象寿命更长,但这至少修补了这个->漏洞。

T& operator*()为了安全起见,您可以选择完全禁止。

shared_reference<T>可以编写A来修补最后一个洞,但operator.尚不可用。

同样,aoperator shared_ptr<T>()会很好,也是一种.lock()方法,允许临时多行所有权。甚至explicit operator bool()可能会遇到共享指针和文件操作所具有的“检查,然后执行,但执行之前的检查可能无效”问题。

于 2016-03-11T20:52:55.817 回答