[英]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
负责节点和边的生存期,并负责确保Node
和Edge
中的指针不悬空。 但是,如果程序员不这样做,则存在不确定行为的风险。
但是由于引用计数的开销成本,人们可以强烈要求使用智能指针不会发生任何未定义的行为。 相反,它将优雅地崩溃。 它保证了这种情况尽早发生(避免破坏更多数据),并且不会被忽视。 这是一种可能的实现:
(编辑:固定的实现, 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.