为什么unique_ptr和shared_ptr不会使它们构造的指针失效?

4
注意:这是一个API设计问题,基于unique_ptrshare_ptr构造函数的设计,仅用于问题描述,并不旨在提出对其当前规格的任何更改。
尽管使用make_uniquemake_shared通常是明智的,但是unique_ptrshared_ptr都可以从原始指针构造。
两者都通过值获取指针并将其复制。它们都允许(即:不阻止)在构造函数中传递的原始指针继续使用。
以下代码编译并导致双重释放:
int* ptr = new int(9);
std::unique_ptr<int> p { ptr };
// we forgot that ptr is already being managed
delete ptr;

unique_ptrshared_ptr都可以避免上述问题,如果它们的相关构造函数期望将原始指针作为rvalue传递,例如对于unique_ptr:

template<typename T>
class unique_ptr {
  T* ptr;
public:
  unique_ptr(T*&& p) : ptr{p} {
    p = nullptr; // invalidate the original pointer passed
  }
  // ...

因此,原始代码将无法编译,因为lvalue无法绑定到rvalue上,但是使用std::move,代码可以编译,而且更加冗长和安全。
int* ptr = new int(9);
std::unique_ptr<int> p { std::move(ptr) };
if (!ptr) {
  // we are here, since ptr was invalidated
}

很明显,用户使用智能指针时可能会遇到许多其他的错误。常见的论点是“你应该知道如何正确地使用语言提供的工具”,以及“C++不是为了看守你而设计的”等等。
但是,似乎仍然有一种防止这种简单错误并鼓励使用make_shared和make_unique的选项。即使在C++14之前添加了make_unique,也仍然存在直接分配而不使用指针变量的选项,如下所示:
auto ptr = std::unique_ptr<int>(new int(7));

似乎将指针作为构造函数参数请求“右值引用”可能会增加一些额外的安全性。此外,获取“右值”的语义似乎更准确,因为我们拥有传递的指针。
现在谈到为什么标准没有采用这种更安全的方法?
一个可能的原因是,上述建议的方法将防止从const指针创建unique_ptr,也就是说,使用所提出的方法编译以下代码会失败:
int* const ptr = new int(9);
auto p = std::unique_ptr { std::move(ptr) }; // cannot bind `const rvalue` to `rvalue`

但我认为这似乎是一个值得忽略的罕见情况。

或者,如果需要支持从const指针初始化是反对提议方法的一个有力论据,那么仍然可以通过以下方式实现更小的改进:

unique_ptr(T* const&& p) : ptr{p} {
    // ...without invalidating p, but still better semantics?
}

3
因为您不应该使用 new,应该使用 std::make_unique 或 std::make_shared。 - MrTux
通常情况下,存在其他值相等的指针。您如何将它们设置为“nullptr”? - Caleth
@MrTux 我认为所提出的方法会更加鼓励使用std::make_unique或std::make_shared,但由于已经有一个允许从原始指针创建的构造函数,也许它应该被定义得不同 - 这就是问题所在。 - Amir Kirsh
如果Howard Hinnant看到了这个问题,或许可以直接发表他的意见? - JohnFilleau
2
通过原始指针的副本引用所拥有的对象从来没有被视为错误,或者甚至是危险的。有时候它甚至是有益的:https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ - Howard Hinnant
显示剩余12条评论
2个回答

4
只要它们都有一个名为get()的方法,使原始指针失效并不是一种"更安全"而是一种更加混淆的方法。在C++中使用裸指针时,它们本身不代表任何所有权概念,也不定义对象生命周期。在C++中使用裸指针的人需要知道,只有在对象存在时它们才有效(无论对象的生命周期是由智能指针还是程序逻辑强制执行)。从裸指针创建智能指针并不会"转移"对象的所有权,而是"分配"它。这个区别可以在下面的例子中得到清晰的说明:
std::unique_ptr<int> ptr1 = std::make_unique<int>(1);
int* ptr2 = ptr1.get();
// ...
// somewhere later:
std::unique_ptr<int> ptr3(ptr2);
// or std::unique_ptr<int> ptr3(std::move(ptr2));

在这种情况下,int对象的所有权没有转移给ptr3。它被误赋值给了ptr1而没有释放所有权。程序员需要知道传递给std::unique_ptr构造函数的指针来自哪里,以及到目前为止如何强制执行对象的所有权。虽然库可能保证会使特定的指针变量无效,但这并不提供任何真正的安全性,可能会让程序员产生虚假的安全感。

即使它们没有 get,也总有 &*ptrptr::operator-> - Caleth
这是一个论点:“用户有各种方式编写错误,他们应该知道如何正确使用语言提供的工具,C++并不是为了监视你。” - Amir Kirsh

1
我认为答案很简单:零开销。这种改变并不是unique_ptr实现所必需的,因此标准并不要求它。如果您认为这样做足以提高安全性,那么可以要求您的实现添加它(可能在特殊的编译标志下)。
顺便说一句,我希望静态分析器能够了解足够的信息来警告这种代码模式。

1
提议的版本 unique_ptr(T* const&& p) : ptr{p} 不会使原始指针失效,仍然是 零开销,但增加了一定程度的安全性,不允许传递左值指针。无论如何,这不是改变当前实现的建议,而是一个API设计问题。看起来这可能是 const右值引用 的一个很好的用法。 - Amir Kirsh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接