为什么原始指针没有"weak pointer"?还是有?

Why is there no "weak pointer" for raw pointer? Or is there?

本文关键字:原始 pointer weak 指针 为什么      更新时间:2023-10-16

毫无疑问,共享指针是个好主意。但是只要一个大型程序包含原始指针,我认为使用共享指针就有很大的风险。主要是,您将失去对指向包含原始指针的对象的指针的实际生命周期的控制,并且错误将发生在更难查找和调试的位置。

所以我的问题是,有没有尝试在现代 c++ 中添加一个不依赖于使用共享指针的"弱指针"?我的意思只是一个指针,当在程序的任何部分删除时,它会变为 NULL。有理由不使用这种自制包装器吗?

为了更好地解释我的意思,以下是我所做的这样一个"弱点"。我把它命名为WatchedPtr。

#include <memory>
#include <iostream>
template <typename T>
class WatchedPtr {
public:
// the only way to allocate new pointer
template <typename... ARGS>
WatchedPtr(ARGS... args) : _ptr (new T(args...)), _allocated (std::make_shared<bool>(true)) {}
WatchedPtr(const WatchedPtr<T>& other) : _ptr (other._ptr), _allocated (other._allocated) {}
// delete the pointer
void del () {delete _ptr; *_allocated = false;}
auto& operator=(const WatchedPtr<T> &other) { return *this = other; }
bool isNull() const { return *_allocated; }
T* operator->() const { return _ptr; }
T& operator*() const { return *_ptr; }
private:
T* _ptr;
std::shared_ptr <bool> _allocated;
};
struct S {
int a = 1;
};
int main () {
WatchedPtr<S> p1;
WatchedPtr<S> p2(p1);
p1->a = 8;
std::cout << p1.isNull () << std::endl;
std::cout << p2.isNull () << std::endl;
p2.del ();
std::cout << p1.isNull () << std::endl;
std::cout << p1.isNull () << std::endl;
return 0;
}

结果:

1
1
0
0

-编辑-

谢谢大家。到目前为止的评论和答案之后的一些澄清:

  • 我为 WatchedPtr 提供的实现只是为了演示我的意思:一个指针,它不会从外部分配中获取副本,无法在外部删除,如果删除,则变为 null。该实现明知远非完美,也不意味着完美。
  • 混合shared_ptr和原始指针的问题非常普遍:A* 被保留为原始指针,因此在程序的某个点创建,并在程序的某个点显式删除。B 持有 A*,而 B* 保持为shared_ptr,因此 B* 具有模糊的寿命。因此,B在删除B持有的A*之后可能会存活很长时间。
  • 在我看来,"WatchedPtr"的主要用法是防御性编程。 即检查 null 并尽最大努力确保连续性(和调试错误)。 shared_ptr可以做到这一点,但以一种非常危险的方式 - 它会隐藏和延迟问题。
  • 也可以有"WatchedPtr"的设计用法(很少和明确的"所有者"),但这不是主要思想。为此,共享指针确实在做这项工作。
  • "WatchedPtr"的目的不是一次替换程序中所有现有的原始指针。这与更换为shared_ptr的努力不同,恕我直言,整个程序已经一次完成了。这对于大型程序来说是不现实的。

弱指针依赖于来自智能指针基础结构的通知,因此您永远无法使用实际的原始指针执行此操作。

人们可以想象,比如说,支持弱指针的unique_ptr的扩展,当然。据推测,没有人急于实现这样一个功能的主要原因是弱指针已经处于"使用引用计数,一切都应该正常工作"的末端,而unique_ptr处于"通过RAII管理你的生命周期,否则你不是一个真正的C++程序员"的终端。弱指针还要求每个分配有一个单独的控制块,这意味着与shared_ptr相比,这种WatchedPtr的性能优势将是最小的。

我认为使用共享指针有很大的风险。主要是,您将失去对指向包含原始指针的对象的指针的实际生命周期的控制,并且错误将发生在更难查找和调试的位置。

然后你说

只是一个指针,在程序的任何部分删除时变为 NULL。

你没看到矛盾吗?

您不希望使用共享指针,因为对象的生存期是在运行时确定的。目前为止,一切都好。

但是,您希望指针在所有者删除它时自动变为 null。问题是,如果指针的生存期是已知的,则根本不需要它!如果您知道指针的生存期何时结束,那么您应该能够删除该指针的所有实例,从而可以检查指针是否失效。

如果你有一个指针,你不知道所有者什么时候会释放它,并且没有办法检查或没有可观察到的副作用来观察弱所有者的观点,那么你真的可以控制你的指针的生命周期吗?没有。

事实上,您的实现依赖于包含共享指针。从某种意义上说,这是有启发性的,因为您需要某种形式的共享所有权才能实现可以具有弱指针的原始指针。然后,如果您需要共享所有权来实现具有弱引用的原始指针,则只剩下一个共享指针。这就是为什么你提议的类的存在是矛盾的。

std::shared_ptr+std::weak_ptr用于处理"您的程序的某些部分不知道所有者何时释放资源"的问题。您需要的是单个std::shared_ptr和多个std::weak_ptr,以便他们知道何时释放资源。这些类具有在运行时检查变量生存期所需的信息。

或者,如果相反,你知道指针的生命周期,那么利用这些知识并找到一种方法来删除悬空指针,或者公开一种方法来检查悬空指针。

阅读答案和评论,以及Bjarne Stroustrup和Herb Sutter的核心指南C++我得出以下答案:
遵循指南时,不需要涉及"新"和"删除"的"WatchedPtr"。但是,出于调试/QA 目的,跟踪从智能指针获取的原始指针的有效性的方法对我来说仍然存在问题。

详细内容:

应继续使用原始指针。出于各种原因。但是,明确的"新建"和"删除"不应。调用"新"和"删除"的情况都应替换为shared_ptr/unique_ptr。
在当前分配原始指针的位置,将其替换为"WatchedPtr"是没有意义的。
如果将原始指针替换为分配给它的其他内容,则在大多数情况下,它将unique_ptr,而在其他情况下将shared_ptr。"WatchedPtr"(如果有的话)将从该点继续,从共享/唯一指针构建。

因此,我发布了一个有点不同的问题。