存储同一指针的多个shared_ptr

Multiple shared_ptr storing same pointer

本文关键字:shared ptr 指针 存储      更新时间:2023-10-16

考虑这个程序:

#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 中(非规范的)"Note"的建议实现,在类 enable_shared_from_this 中使用私有weak_ptr成员。 如果不在enable_shared_from_this中做一些更复杂的事情,似乎不可能解释这类问题。

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

是的,C++11 中存在缺陷。在允许这样做时:

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

这应该明确声明为未定义的行为,无论"删除者"做什么。 当然,从技术上讲,这样做可能并不违法。

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

您的代码并非如此。所以我想说它在语法上是正确的,但在语义上是无效的。

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

我同意这是规范中的一个漏洞,因此是一个缺陷。它基本上与 http://open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2179 相同,尽管该问题来自略有不同的(恕我直言更明显的破碎)角度。

不确定我是否同意这是对shared_ptr的误用,我认为用shared_ptrs这样做很好,因为与问题 2179 中的代码不同,您使用无操作删除器。我认为问题在于当您尝试将这种shared_ptr的使用与enable_shared_from_this相结合时。

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

要求:enable_shared_from_this<T>应是可访问的T基类。 *this 应该是类型 T t对象的子对象。 至少应有一个拥有&tshared_ptr实例p,拥有&t的任何其他shared_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共享所有权,这超出了范围,使weak_ptr<X>处于过期状态。

有趣的是,boost::shared_ptr 不会抛出,而是 #2 是无操作,#3 返回与xp1共享所有权的shared_ptr。这是为了响应错误报告,其示例与上述示例几乎完全相同。

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

启用带有p shared_­from_­this,对于类型为 Y* 的指针p,意味着如果Y有一个明确且可访问的基类,该基类是 enable_­shared_­from_­this 的专用化,则remove_­cv_­t<Y>*应隐式转换为 T* 并且构造函数计算语句:

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

  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