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 的所有要求:

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

[TXtxpp1。]

但是 g++enable_shared_from_this基本上遵循 20.7.2.4p10 中(非规范)“注释”中建议的实现,使用weak_ptrclass 中的私有成员enable_shared_from_this。如果不做一些相当复杂的事情,似乎不可能解决这类问题enable_shared_from_this

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

4

4 回答 4

6

是的,C++11 有一个缺陷。允许这样做:

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

无论“删除者”做什么,都应该明确说明这是未定义的行为。当然,从技术上讲,以这种方式做事可能并不违法。

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

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

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

于 2012-04-26T18:40:32.587 回答
5

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

我不确定我是否同意这是对shared_ptr. 我认为问题在于当您尝试将这种用法与shared_ptrwith结合使用时enable_shared_from_this

所以我的第一个想法是通过扩展以下要求来解决它shared_from_this

Requires: enable_shared_from_this<T>应该是一个可访问的基类T*this应该是t类型对象的子对象T。应至少有一个拥有的shared_ptr实例p拥有 &t 的任何其他shared_ptr实例&t应与p.

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

该程序的较小形式是:

#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) 行为相同并抛出bad_weak_ptr#3,因为在 #2 他们更新了weak_ptr<X>基类的成员以使其与 共享所有权xp2,这超出了范围而使weak_ptr<X>处于过期状态.

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

于 2015-08-25T10:23:29.590 回答
0

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

启用shared_­from_­thiswith ,对于类型p的指针,意味着如果有一个明确且可访问的基类,它是 的特化,则应隐式转换为并且构造函数评估语句:pY*Yenable_­shared_­from_­thisremove_­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现在不会引发异常,并且两个“非空”检查都将显示为真。由于在调用lock2()internal时weak_ptr已经拥有,因此 not expired()lock2()保持weak_ptr不变,因此第二次调用shared_from_this()返回 a shared_ptrwhich 与 共享所有权p1

于 2018-05-29T12:10:12.083 回答
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
于 2018-06-05T01:27:34.240 回答