【问题标题】:What are the pros and cons of using smart pointers as "non-owning references"?使用智能指针作为“非拥有引用”的优缺点是什么?
【发布时间】:2016-03-11 20:29:48
【问题描述】:

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

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

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

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

【问题讨论】:

  • 拥有非拥有的原始指针没有任何问题。
  • 嗯,这是一个意见。我知道世界级的工程师会不同意这一点。最先进的研究,例如 2015 年的这篇论文:wenke.gtisc.gatech.edu/papers/dangnull.pdf
  • @NathanOliver 所以事实是:有 are 错误的东西与非拥有原始指针。问题是:不使用它们的代价是什么?值得麻烦吗?
  • 好吧this 是我的指导方针。恕我直言,问题是您是否允许用户在脚上射击自己,或者您是否喜欢他们并消耗更多资源。只要您声明传递给您的班级的指针必须比您的班级长,那么责任就在用户身上。
  • 还应该注意的是,核心 C++ 指南还说非拥有指针应该是原始指针。或者更确切地说,如果你有一个原始指针,你就不拥有它。

标签: c++ memory-safety


【解决方案1】:

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

忽略智能指针没有其他问题除了性能和安全性(性能是为什么我们不只是让 GC 安全地处理它),事实上,您的 nonowning_ptr 课程严重损坏。

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

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

线程尤其打破了您对安全的幻想。

所以nonowning_ptr 最重要的“缺点”是它坏了;它并不比T* 更安全。

【讨论】:

  • 谢谢!你知道有没有不坏的替代品?或者如果(除了宏),可以将if(auto sp = lock() ) { sp-&gt;doSomething(); } else { abort(); } 替换为sp-&gt;doSomething();,作为语法糖? (我什至不确定宏是否可行)
  • @Boris:这是一个完全不同的问题。不过不用问了,因为在宏魔法之外,答案是“不”。即使有宏观魔法,我猜也不太可能。
  • @NicolBolas 答案是“是”,因为a-&gt;b魔法。看我的回答。 ;)
  • @Yakk:他没有询问是否返回shared_ptr。他询问了一种使sp-&gt;doSomething 成为迷你函数调用的方法,如果指针不存在,则调用abort。虽然你的建议会达到类似的效果,但这仍然不是他所要求的。
  • @NicolBolas 如果指针不存在,我的解决方案以什么方式不调用abort,如果指针存在,则不调用sp-&gt;doSomething(),这是鲍里斯问题的重点?
【解决方案2】:

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

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

// 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-&gt;foo(); 是(实际上)p-&gt;get()-&gt;foo()get() shared_ptr 返回值的生命周期比对 foo() 的调用长,所以一切都像房子一样安全。

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

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

可以编写shared_reference&lt;T&gt; 来修补最后一个洞,但operator. 尚不可用。

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

【讨论】:

  • 太棒了,谢谢!好吧,这并不能回答实际问题,但非常有用。我可以完全禁止get()T&amp; operator()。我还在我的实际测试和explicit operator bool() 中实现了指针可能确实有效或无效的非拥有情况,但我意识到在这些情况下我宁愿明确地做一个锁,因为它不会影响可读性很多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-09-15
  • 1970-01-01
  • 2015-11-06
  • 1970-01-01
  • 1970-01-01
  • 2017-07-04
  • 1970-01-01
相关资源
最近更新 更多