繁体   English   中英

将智能指针用作“非所有者引用”的利弊是什么?

[英]What are the pros and cons of using smart pointers as “non-owning references”?

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

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;
};

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

我并不是在问哪个最好,当然也没有最好,这取决于用例。 我要问的是每种方法可能存在的利弊,而事实并非如此,这将有助于做出设计决策(也许在可读性,可维护性,可移植性以及与第三方库的配合方面很不错) ( 防止免费使用后的利用? )。

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

忽略这样的事实, 除了性能和安全性 ,智能指针没有其他问题(性能就是为什么我们不仅仅让GC安全地处理它),事实是您的nonowning_ptr类被严重破坏了。

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

销毁了weak_ptr::lock返回的shared_ptr的那weak_ptr::lock ,便删除了使该内存保持有效的唯一方法。 这意味着,如果有人在您拥有T*出现并删除了该内存中的最后一个shared_ptr ,那么您就搞砸了。

特别是穿线会破坏您对安全性的幻想。

因此, nonowning_ptr最重要的“骗局”是它已损坏; 它比T*更安全。

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

为了减少这种情况, T * get()应该是std::shared_ptr<T> get() 并且operator->还应该返回std::shared_ptr<T> 尽管这似乎是不切实际的,但实际上它是由于有趣的方式而起作用的->在C ++中定义为自动递归。 a->定义为(*a).如果a是指针类型,则(a.operator->())->否则。因此,您的->返回shared_ptr ,然后在其上调用-> ,然后返回指针。这样可以确保您正在执行的指针的生命期-> on足够长。)

// 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>来修补最后一个漏洞,但要operator. 尚不可用。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM