多个shared_ptr存储相同指针

12

考虑下面的程序:

#include <memory>
#include <iostream>

class X
  : public std::enable_shared_from_this<X>
{
public:
  struct Cleanup1 { void operator()(X*) const; };
  struct Cleanup2 { void operator()(X*) const; };
  std::shared_ptr<X> lock1();
  std::shared_ptr<X> lock2();
};

std::shared_ptr<X> X::lock1()
{
  std::cout << "Resource 1 locked" << std::endl;
  return std::shared_ptr<X>(this, Cleanup1());
}

std::shared_ptr<X> X::lock2()
{
  std::cout << "Resource 2 locked" << std::endl;
  return std::shared_ptr<X>(this, Cleanup2());
}

void X::Cleanup1::operator()(X*) const
{
  std::cout << "Resource 1 unlocked" << std::endl;
}

void X::Cleanup2::operator()(X*) const
{
  std::cout << "Resource 2 unlocked" << std::endl;
}

int main()
{
  std::cout << std::boolalpha;

  X x;
  std::shared_ptr<X> p1 = x.lock1();
  {
    std::shared_ptr<X> p2 = x.lock2();
  }
}

我在C++11标准的20.7.2节中没有看到任何暗示这些都是无效的。虽然两个shared_ptr对象存储相同的指针x但不共享所有权并且使用不会结束get()生命周期的删除器有点不寻常,但没有什么能禁止它。 (如果其中任何一个完全是无意的,那么很难解释为什么一些shared_ptr成员函数接受std::nullptr_t值。)正如预期的那样,程序输出:
Resource 1 locked
Resource 2 locked
Resource 2 unlocked
Resource 1 unlocked

但是,如果我在 main() 函数中添加一些内容:

int main()
{
  std::cout << std::boolalpha;

  X x;
  std::shared_ptr<X> p1 = x.lock1();
  bool test1( x.shared_from_this() );
  std::cout << "x.shared_from_this() not empty: " << test1 << std::endl;
  {
    std::shared_ptr<X> p2 = x.lock2();
  }
  try {
    bool test2( x.shared_from_this() );
    std::cout << "x.shared_from_this() not empty: " << test2 << std::endl;
  } catch (std::exception& e) {
    std::cout << "caught: " << e.what() << std::endl;
  }
}

然后事情就变得更加棘手了。使用g++ 4.6.3,我得到的输出如下:

Resource 1 locked
x.shared_from_this() not empty: true
Resource 2 locked
Resource 2 unlocked
caught: std::bad_weak_ptr
Resource 1 unlocked

为什么第二次调用 shared_from_this() 会失败?所有20.7.2.4p7的要求都已满足:

要求: enable_shared_from_this<T> 应该是类 T 的一个可访问基类。 *this 应该是类型为 T 的对象 t 的子对象。至少有一个 shared_ptr 实例 p 拥有 &t

[TXtxpp1。]

但是,g++的 enable_shared_from_this 实际上遵循了20.7.2.4p10中(非规范性)“注释”中建议的实现方式,使用了类 enable_shared_from_this 中的私有 weak_ptr 成员。而且似乎不可能在 enable_shared_from_this 中做到这种问题的解决,除非采取更加复杂的方法。

这是标准中的缺陷吗?(如果是,就不需要在此评论应该采取什么解决方案:添加一个要求,使示例程序调用未定义行为,更改注释以不建议使用这样简单的实现方法,...)


1
从标准的角度来看,我不确定。原因是g++(和boost)实现期望您只在第一次从给定的X类的原始指针实例创建共享指针时设置弱引用私有变量,并且该弱引用指向已创建的实例。当您在lock2()中对同一实例创建第二个新共享指针时,它将覆盖原始弱指针,当它解锁时,弱指针现在指向无效地址,因此出现错误。 - Dave S
非规范性说明中展示了enable_from_this的一个实现示例,其中第11段结论是:“创建独占指针的shared_ptr构造函数可以检测到enable_shared_from_this基类的存在,并将新创建的shared_ptr分配给其__weak_this成员。”[我强调] 我觉得这个说明没有用“创建拥有指针”的说法来表达,这让我想知道什么是独特的shared_ptr - Luc Danton
1
@Luc 我相信这是指那些在创建后从它们的 unique() 返回 true 的构造函数。基本上,这些构造函数从原始指针或 unique_ptr 中获取初始所有权。 - Dave S
2
@LucDanton:一个独特的shared_ptr是被定义明确的;它的shared_ptr::unique方法返回true。即:use_count() == 1shared_ptr - Nicol Bolas
4个回答

6

是的,在C++11中存在一个缺陷。它允许以下情况:

有两个shared_ptr对象存储相同的指针&x,但不共享所有权,并使用不会结束*get()生命周期的"删除器"。这有点不寻常,但没有任何禁止。

无论"删除器"做什么,都应该明确说明这是未定义行为。事实上,以这种方式进行操作可能在技术上不违法。

然而,你向使用代码的人们 撒谎 。任何收到shared_ptr的人的期望都是他们现在拥有了该对象的所有权。只要他们保留那个shared_ptr(或其副本),它所指向的对象就仍然存在。

但是,对于你的代码,情况并非如此。因此,我认为它在语法上是正确的,但在语义上是无效的。

shared_from_this的语言很好。需要改变的是shared_ptr的语言。应该声明创建两个单独的唯一指针"拥有"相同指针是未定义行为。


shared_ptr<Y> p1(nullptr, d1); shared_ptr<Z> p2(nullptr, d2); 这段代码同样是无效的吗? - aschepler
1
@aschepler:这就是为什么我说标准应该声明它为未定义行为。它是否可能工作并不重要;它是错误的,应该被禁止。 - Nicol Bolas
"@aschepler“_给x静态存储期_”仍然不是正确的语义:持有拥有“智能指针”(shared_ptrunique_ptr...)就像是对象生命周期上的锁定,应该足以保持目标,但具有非平凡析构函数的静态对象仍将在程序执行期间某个时刻被销毁,除非析构函数体仅包含对_exit()的调用,否则销毁是可观察的(进入析构函数体正式结束生命周期,但实际上什么也没做)。" - curiousguy
@NicolBolas "因为shared_ptr不拥有它。生命周期由其他地方控制" 在“共享所有权”的情况下,“控制”是主观的,与独占所有权(unique_ptr以及.unique()shared_ptr)不同;独占所有权是具有法律所有权属性的真正所有权:它可以被转让。(虽然shared_ptr不支持.release()操作,但在前提条件.unique()下可以实现。)另一方面,共享所有权是一个非常模糊的概念,因为你不知道谁分享它和他们的意图。在这种情况下,你没有控制权。 - curiousguy
@aschepler 停止程序以“保留”程序不变量可能并非在所有情况下都是可接受的方法。这就像assert或throw-specifications一样:停止比运行在违反设计原则的状态下更好。 - curiousguy
显示剩余5条评论

5

我同意这是规范中的一个漏洞,因此是一个缺陷。虽然该问题从略微不同(并且我认为更明显有问题)的角度http://open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2179,但基本上是相同的。

我不确定我是否同意这是对shared_ptr的错误使用,我认为使用shared_ptrs来做这件事是可以的,因为与问题2179中的代码不同,你使用了no-op deleters。 我认为问题在于当你尝试将这种使用shared_ptrenable_shared_from_this结合使用时会出现问题。

因此,我的第一个想法是通过扩展shared_from_this的要求来修复它:

要求: enable_shared_from_this<T> 必须是 T 的一个可访问的基类。 *this 必须是类型为 T 的对象 t 的子对象。 至少应该有一个 shared_ptr 实例 p,它拥有 &t 和任何其他拥有 &tshared_ptr 实例都应该与 p 共享所有权。

然而,这还不够,因为您的示例符合该要求:在第二次调用 shared_from_this() 时,只有一个所有者(p1),但您已经通过调用 lock2() “破坏”了 enable_shared_from_this 基类的状态。

程序的较小形式如下:

#include <memory>
using namespace std;

int main()
{
  struct X : public enable_shared_from_this<X> { };
  auto xraw = new X;
  shared_ptr<X> xp1(xraw);   // #1
  {
    shared_ptr<X> xp2(xraw, [](void*) { });  // #2
  }
  xraw->shared_from_this();  // #3
}

所有的libstdc++、libc++和VC++(Dinkumware)都表现相同,在#3处抛出bad_weak_ptr异常,因为在#2处它们更新基类的weak_ptr<X>成员,使其与xp2共享所有权,而xp2超出作用域后,weak_ptr<X>处于过期状态。

有趣的是,boost::shared_ptr不会抛出异常,而是#2是一个无操作,并且#3返回一个与xp1共享所有权的shared_ptr。这是针对一个错误报告做出的反应,该错误报告几乎与上面的示例完全相同。

0
  X x;
  std::shared_ptr<X> p1 = x.lock1();
  (...sniped...)
}

这样的代码破坏了“拥有”“智能指针”的语义:

  • 它们可以被复制
  • 只要保留一个副本,所拥有的对象就会一直存在

这个不变量是如此重要,以至于我认为这样的做法应该在代码审查中被拒绝。但是有一种你所建议的变体满足了这个不变量:

  • 对象必须是动态管理的(因此不是自动的)
  • 任何拥有对象的家族都共同拥有动态管理的对象
  • 每个家族成员都共同拥有该家族的“删除器”

因此,这里有共享拥有对象,它们是不同的拥有对象“家族”的一部分,它们不是“等价”的,因为它们具有不同的:

  • “删除器”对象
  • use_count()
  • 控制块
  • owner_before 结果

但是它们都防止相同对象的销毁;这是通过在每个“删除器”对象中保留 shared_ptr 的副本来完成的。

使用一个干净的替代品来替换std::shared_from_this,以完全控制std::weak_ptr<T>成员的初始化。

#include <memory>
#include <iostream>
#include <cassert>

// essentially like std::shared_from_this
// unlike std::shared_from_this the initialization IS NOT implicit
// calling set_owner forces YOU to THINK about what you are doing!

template <typename T>
class my_shared_from_this
{
    std::weak_ptr<T> weak;
public:
    void set_owner(std::shared_ptr<T>);
    std::shared_ptr<T> shared_from_this() const;
};

// shall be called exactly once
template <typename T>
void my_shared_from_this<T>::set_owner(std::shared_ptr<T> shared)
{
    assert (weak.expired());
    weak = shared;
}

template <typename T>
std::shared_ptr<T> my_shared_from_this<T>::shared_from_this() const
{
    assert (!weak.expired());
    return weak.lock();
}

class X : public my_shared_from_this<X>
{
public:
  struct Cleanup1 { 
    std::shared_ptr<X> own;
    Cleanup1 (std::shared_ptr<X> own) : own(own) {}
    void operator()(X*) const; 
  };

  struct Cleanup2 { 
    std::shared_ptr<X> own;
    Cleanup2 (std::shared_ptr<X> own) : own(own) {}
    void operator()(X*) const; 
  };

  std::shared_ptr<X> lock1();
  std::shared_ptr<X> lock2();

  X();
  ~X();
};

// new shared owner family with shared ownership with the other ones
std::shared_ptr<X> X::lock1()
{
  std::cout << "Resource 1 locked" << std::endl;
  // do NOT call set_owner here!!!
  return std::shared_ptr<X>(this, Cleanup1(shared_from_this()));
}

std::shared_ptr<X> X::lock2()
{
  std::cout << "Resource 2 locked" << std::endl;
  return std::shared_ptr<X>(this, Cleanup2(shared_from_this()));
}

void X::Cleanup1::operator()(X*) const
{
  std::cout << "Resource 1 unlocked" << std::endl;
}

void X::Cleanup2::operator()(X*) const
{
  std::cout << "Resource 2 unlocked" << std::endl;
}

X::X()
{
  std::cout << "X()" << std::endl;
}

X::~X()
{
  std::cout << "~X()" << std::endl;
}

// exposes construction and destruction of global vars
struct GlobDest {
  int id;
  explicit GlobDest(int id);
  ~GlobDest();
};

GlobDest::GlobDest(int id) 
  : id(id) 
{
    std::cout << "construction of glob_dest #" << id << std::endl;
}

GlobDest::~GlobDest() {
    std::cout << "destruction of glob_dest #" << id << std::endl;
}

GlobDest glob_dest0 {0};
std::shared_ptr<X> glob;
GlobDest glob_dest1 {1};

std::shared_ptr<X> make_shared_X()
{
    std::cout << "make_shared_X" << std::endl;
    std::shared_ptr<X> p = std::make_shared<X>();
    p->set_owner(p);
    return p;
}

int test()
{
  std::cout << std::boolalpha;

  std::shared_ptr<X> p = make_shared_X();
  static std::shared_ptr<X> stat;
  {
    std::shared_ptr<X> p1 = p->lock1();
    stat = p1;
    {
      std::shared_ptr<X> p2 = p->lock2();
      glob = p2;
      std::cout << "exit scope of p2" << std::endl;
    }
    std::cout << "exit scope of p1" << std::endl;
  }
  std::cout << "exit scope of p" << std::endl;
}

int main()
{
  test();
  std::cout << "exit main" << std::endl;
}

输出:

construction of glob_dest #0
construction of glob_dest #1
make_shared_X
X()
Resource 1 locked
Resource 2 locked
exit scope of p2
exit scope of p1
exit scope of p
exit main
Resource 1 unlocked
destruction of glob_dest #1
Resource 2 unlocked
~X()
destruction of glob_dest #0

0

这个问题和其他相关问题在C++17中得到了澄清。现在,std::enable_shared_from_this<T>被指定为具有单个std::weak_ptr<T> weak_this;成员。对于非数组的std::shared_ptr专业化,该成员由std::shared_ptr构造函数、std::make_sharedstd::allocate_shared分配,如[util.smartptr.shared.const]/1所述:

Enables shared_­from_­this with p, for a pointer p of type Y*, means that if Y has an unambiguous and accessible base class that is a specialization of enable_­shared_­from_­this, then remove_­cv_­t<Y>* shall be implicitly convertible to T* and the constructor evaluates the statement:

if (p != nullptr && p->weak_this.expired())
  p->weak_this = shared_ptr<remove_cv_t<Y>>(*this, const_cast<remove_cv_t<Y>*>(p));
所以,我的 OP 中第二个 main 的正确行为现在是不会抛出任何异常,并且两个“非空”检查都将显示为 true。由于在调用 lock2() 时,内部的 weak_ptr 已经被拥有,因此不是 expired()lock2() 不会改变 weak_ptr,因此对 shared_from_this() 的第二次调用返回一个与 p1 共享所有权的 shared_ptr

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