【问题标题】:Why unique_ptr and shared_ptr do not invalidate the pointer they are constructed from?为什么 unique_ptr 和 shared_ptr 不会使构造它们的指针无效?
【发布时间】:2020-06-17 11:23:45
【问题描述】:

注意:这是一个 API 设计问题,为了这个问题,依赖于 unique_ptrshare_ptr 的构造函数的设计,但是不打算对他们当前的规范提出任何改变。


虽然通常建议使用make_uniquemake_shared,但unique_ptrshared_ptr 都可以从原始指针构造。

两者都通过值获取指针并复制它。两者都允许(即:不阻止)在构造函数中继续使用传递给它们的原始指针。

以下代码编译结果为double free:

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_sharedmake_unique。甚至在 C++14 中添加 make_unique 之前,仍然可以选择直接分配而无需指针变量,如:

auto ptr = std::unique_ptr<int>(new int(7));

似乎将右值引用作为构造函数参数请求指针可以增加一点额外的安全性。此外,获取 rvalue 的语义似乎更准确,因为我们获得了传递的指针的所有权。

关于为什么标准不采用这种更安全的方法?

的问题

可能的原因是上面建议的方法会阻止从 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?
}

【问题讨论】:

  • 因为你根本不应该使用 new,所以使用 std::make_unique 或 std::make_shared
  • 一般情况是有other个值相等的指针。你如何将它们设置为'nullptr'?
  • @Caleth 这不会解决指向同一地址的其他指针的问题。理论上这是一般的情况,实际上远不是普通的情况。无论如何,在传递指针变量的情况下要求用户调用std::move 的语义使得传递所有权变得更加冗长
  • @MrTux 我会说提议的方法会鼓励更多使用 std::make_unique 或 std::make_shared,但因为已经有一个构造函数允许从原始指针创建也许它应该有不同的定义——这就是问题所在
  • 通过原始指针的副本引用拥有的对象从未被认为是错误的,甚至是危险的。有时甚至是有益的:herbsutter.com/2013/06/05/…

标签: c++ shared-ptr api-design unique-ptr rvalue-reference


【解决方案1】:

只要它们都有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 对象的所有权不会转移到ptr3ptr1 在没有释放所有权的情况下错误地将其分配给它。程序员需要知道传递给std::unique_ptr 构造函数的指针来自哪里,以及到目前为止对象的所有权是如何被强制执行的。库保证它将使这个特定的指针变量无效可能会给程序员一种错误的安全感,但不会提供任何真正的安全性。

【讨论】:

  • 即使他们没有get,也总是有&amp;*ptrptr::operator-&gt;
  • 所以这是一个论点:用户有各种写bug的方法,他们应该知道如何正确使用语言提供的工具,C++不是用来监视你的
【解决方案2】:

我认为答案很简单:零开销。 unique_ptr 不需要此更改才能正常工作,因此标准不需要它。如果您认为这足以提高安全性,值得,您可以要求您的实现添加它(可能在一个特殊的编译标志下)。

顺便说一句,我希望静态分析器知道的足够多,可以针对此 Code Pattern 发出警告。

【讨论】:

  • 建议的版本unique_ptr(T* const&amp;&amp; p) : ptr{p} 没有使原始指针失效,仍然是零开销,但增加了一定程度的安全性,不允许传递左值指针。无论如何,这不是改变当前实现的提议,而是一个 API 设计问题。看来这可能是 const rvalue reference 的一个很好的用法。
猜你喜欢
  • 2021-07-15
  • 2021-08-28
  • 2021-06-01
  • 2013-12-17
  • 1970-01-01
  • 2018-05-22
  • 2015-11-28
  • 1970-01-01
  • 2014-02-25
相关资源
最近更新 更多