使用显式实例化声明删除反向指针会导致 std::bad_weak_ptr 异常



我开发了一些正确编译的代码,但在(调试(运行时失败。我正在使用VS2015。

背景:我正在构建一个高级消息引擎。为了使新消息的编程添加可维护性,在生产代码中,我花时间使用explicit initialization declaration C++构造来制作初始消息。这行得通,使新消息的制作千篇一律,更不用说将消息传递的维护减少到几乎为零。以下是此功能的框架代码:

#include <memory>
template< typename D_T >
struct H // prototype for all explicit initialization declarations (EID)
{
  H( D_T& d ) : x { d } {}
  D_T& x;
};
template< typename D_T >
struct B  // base class for derived objects D1 and D2
{
  B( D_T& d ) : d { d } {}
  D_T& d; // a kind of backptr initialized when the EIDs are contructed
  // actual EIDs a and b
  H< D_T > a { d };
  H< D_T > b { d };
};
struct D1 : public B< D1 >
{
  D1() : B( *this ) {}
  void Func1() {}
};
struct D2 : public B< D2 >
{
  D2() : B( *this ) {}
  void Func2() {}
};

int main()
{
  D1 d1;
  D2 d2;
  // as designed either derived object can access either explicitly initialized member a or b 
  d1.a.x.Func1(); // OK
  d1.b.x.Func1(); // OK
  d2.a.x.Func2(); // OK
  d2.b.x.Func2(); // OK
  return 0;
}

此代码编译并运行。

但是我在真实代码中的派生对象是共享的 ptrs。因此,我将此功能添加到代码中。请注意,我使用 enable_shared_from_this 构造获取派生类的 this ptr:

#include <memory>
template< typename D_T >
struct H
{
  H( std::shared_ptr< D_T >& d ) : x { d } {}
  std::shared_ptr< D_T >& x;
};
template< typename D_T >
struct B
{
  B( std::shared_ptr< D_T >& d ) : d { d } {}
  std::shared_ptr< D_T >& d;
  H< D_T > a { d }; // a is initialized with D1
  H< D_T > b { d };
};
struct D1: public std::enable_shared_from_this< D1 >, public B< D1 >
{
  D1() : B( shared_from_this() ) {} // runtime error: bad weak prt
  void Func1() {}
};
struct D2: public std::enable_shared_from_this< D2 >, public B< D2 >
{
  D2() : B( shared_from_this() ) {}
  void Func2() {}
};
int main()
{
  D1 d1;
  D2 d2;
  d1.a.x->Func1();
  d1.b.x->Func1();
  d2.a.x->Func2();
  d2.b.x->Func2();
  return 0;
}

此代码将编译。但是,它不会运行,并且在 D1 构造函数中,它会中断,但异常 std::bad_weak_ptr。

我试图将共享的 ptrs 更改为弱 ptrs,但没有成功。有人看到问题了吗?

编辑 1:根据 @pat 的观察,shared_from_this() 无法从构造函数调用,请参阅下面修改后的代码,该代码现在编译并运行:

#include <memory>
template< typename D_T >
struct H
{
  H( D_T& d ) : x { d } {}
  D_T& x;
};
template< typename D_T >
struct B
{
  B( D_T& d ) : d { d } {}
  D_T& d;
  H< D_T > a { d };
  H< D_T > b { d };
};
struct D1 : public std::enable_shared_from_this< D1 >, public B< D1 >
{
  D1() : B( *this ) {}
  void Func1() {}
};
struct D2 : public std::enable_shared_from_this< D1 >, public B< D2 >
{
  D2() : B( *this ) {}
  void Func2() {}
};
int main()
{
  D1 d1;
  D2 d2;
  d1.a.x.Func1();
  d1.b.x.Func1();
  d2.a.x.Func2();
  d2.b.x.Func2();
  return 0;
}

编辑 2:下面的代码是我原始帖子代码的重写,并建立在@pat的答案之上。以下是更改的内容:显式实例化声明 (EID( 已移动到其派生类。B 不再尝试引用派生对象。这是一个明显的错误。作为后向指针的weak_ptr被简单的后 ptr 取代(就像原型中的情况一样(。泄漏没有问题,因为派生对象(D1 和 D2(完全拥有对象。(在生产代码中,成员类型是共享的 ptr 以防止泄漏。

#include <memory>
#include <cassert>
template< typename D_T >
struct H
{
  H( D_T* d ) : x { d } {}
  D_T* x;
  int qq { 0 };
};
struct B
{
  B() {}
  int rr { 0 };
};
struct D1 : public B
{
  H< D1 > a { this }; // explicit instantiation declaration 
  int ss { 0 };
};
struct D2 : public B
{
  H< D2 > b { this }; // explicit instantiation declaration
  int tt { 0 };
};

int main()
{
  D1 d1;
  D2 d2;
  d1.rr = 99;
  d2.b.x->rr = 88;
  assert( d1.rr == d1.a.x->rr ); // OK
  assert( d2.rr == d2.b.x->rr ); // OK
  return 0;
}

添加任意数量的 EID 时,代码维护复杂性从指数(如原型中的情况(降低到线性的设计不变性已经实现。

对象必须由共享指针管理,shared_from_this才能工作。 在 C++14 中,在尚未由shared_ptr管理的对象上调用 shared_from_this 实际上是未定义的行为。 因此,您将无法从构造函数调用shared_from_this,因为此时对象不在shared_ptr内。

来自 cpp首选项的示例...

struct Good: std::enable_shared_from_this<Good>
{
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};
// Bad: shared_from_this is called without having std::shared_ptr owning the caller 
try {
    Good not_so_good;
    std::shared_ptr<Good> gp1 = not_so_good.getptr();
} catch(std::bad_weak_ptr& e) {
    // undefined behavior (until C++17) and std::bad_weak_ptr thrown (since C++17)
    std::cout << e.what() << 'n';    
}

C++中的对象具有自动生存期或动态生存期。

自动生命周期的变量不能由shared_ptr有意义地管理,除非有一个奇怪的"删除器"。 自动生存期也可以称为"在堆栈上"。

你的代码有一大堆误解。

首先,引用很少延长生存期。 在类中存储std::shared_ptr<X>&几乎总是一个坏主意;它不会延长任何东西的寿命,更不用说X了:即使是shared_ptr的寿命也不会延长。

B( shared_from_this() )

shared_from_this()创建一个shared_ptr<T>,而不是一个shared_ptr<T>&,把它传递给B的构造函数,期望一个引用是无稽之谈。 这完全编译是MSVC2015中的一个缺陷,默认情况下它实现了一个扩展,允许将右值转换为引用。

您在构造函数中调用它的事实也意味着它无法工作。 shared_from_this()调用.lock()存储在enable_shared_from_this中的weak_ptr上,一旦this实际使用shared_ptr进行管理,就会填充。

这几乎总是在创建对象之前不会发生。 在您的情况下,它永远不会发生,但即使您在make_shared中创建它,它也会在对象构造函数完成后发生。

备份时,您在第一个代码中存储& s是不好的。 将&存储在类型中会为其提供分配和复制的引用语义,除非您真正理解它的含义,否则这样做是非常值得怀疑的。 &应该是*this的事实意味着你做错了:复制构造或赋值不会保持这种不变性。

底部共享指针

设计中也会出现类似的问题;重新放置这些共享指针不会自动发生,因此默认情况下它们会做错误的事情。

拥有一个带有指向自身的共享指针的对象是一个可怕的想法;它是一个不朽的对象,因为它为自己提供了自己的生命。

老实说,我几乎不知道这段代码应该做什么,因为据我所知,它毫无目的地做有害的事情。 也许您正在尝试让成员对象知道拥有它们的对象是谁? 并以某种方式将其附加到高级消息传递系统的要求上?

如果您的目标是广播/听众系统,那么问题就变成了您的要求到底是什么? 您的要求越弱,系统就越简单,复杂性会产生巨大的成本。

如果您的大多数广播公司都有少数听众,这些听众的变化频率不会比广播频率更频繁,那么一个简单的广播器/听众系统将弱指针存储在广播器中,并在听众中存储共享指针令牌将解决您的问题。

using token = std::shared_ptr<void>;
template<class...Args>
struct broadcaster {
  using invoker = std::function<void(Args...)>;
  using sp_message = std::shared_ptr<invoker>;
  using wp_message = std::weak_ptr<invoker>;
  token register( sp_message msg ) {
    listeners.push_back(msg);
    return msg;
  }
  token register( invoker f ) {
    auto msg = std::make_shared<invoker>(std::move(f));
    return register( std::move(msg) );
  }
  void operator()( Args...args )
  {
    auto it = std::remove_if( listeners.begin(), listeners.end(),
      [](auto&& ptr) { return !ptr.lock(); }
    };
    listeners.erase(it, listeners.end());
    auto tmp = listeners;
    for (auto&& target:tmp)
      if (auto pf = target.lock())
        (*pf)(args...);
  }
private:
  std::vector<wp_message> listeners;
};

代码未测试。

在这里,我们的broadcaster<int> b;可以std::function<void(int)> f传递给它。 它返回一个shared_ptr<void>又名token。 只要shared_ptr<void>仍然存在,呼叫b(7)就会呼叫f(7)

或者,您可以传递shared_ptr<std::function<void(int)>> . 然后,只要该shared_ptr返回的token仍然存在,侦听器就会被广播到。 (这允许您将生命周期与其他shared_ptr联系起来(

它清理它的listeners,在每次广播之前删除死的。

如果广播员在听众之前死亡,听众就不会得到通知(除非你设置了一个广播公司来准确说! 不包括消息的来源,除非它包含在broadcaster的签名中。

token不依赖于签名;因此,一个一生都听许多broadcaster的类可以有一个std::vector<token>

如果它需要跟踪其广播器的生命周期,我们可以编写一个销毁广播器:

struct on_destroy:broadcaster<on_destroy const*> {
  ~on_destroy() {
    (*this)(this);
  }
};

然后我们可以添加一个监听的东西:

struct listen_until_gone {
  template<class...Args>
  void register( on_destroy& d, broadcaster<Args...>& b, std::function<void(Args...)> m )
  {
    auto it = listeners.find(&d);
    if (it != listeners.end()) {
      listeners[&d] = {d.register( [this](on_destroy const*d){
        this->listeners.erase(d);
      })};
    }
    listeners[&d].push_back(
      b.register( std::move(m) )
    );
  }
private:
  std::unordered_map< on_destroy const*, std::vector<token> > listeners;
};

现在,侦听器可以拥有listen_until_gone listen;

要收听具有on_destroy的给定广播公司,我们这样做:

listen.register(
  bob.on_destroy, bob.name_change,
  [this]( std::string const& new_name ){
    this->bob_name_changed(new_name);
  }
);

然后忘记它。

但是,如果广播公司倾向于比听众活得更久,我只是收听并将其存储在向量中。

最新更新