使用显式实例化声明删除后向指针会导致 std::bad_weak_ptr 异常
Removing back pointers with the use of explicit instantiation declarations causes std::bad_weak_ptr exception
我开发了一些可以正确编译但在(调试)运行时失败的代码。我正在使用 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;
}
此代码编译并运行s。
但是我在真实代码中的派生对象是共享指针。因此,我将此功能添加到代码中。请注意,我正在使用 enable_shared_from_this
构造获取派生的 class'es 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。
我尝试将共享指针更改为弱指针,但没有成功。有人看到问题了吗?
编辑 1:
根据@pat 的观察,shared_from_this()
不能从构造函数调用,请参阅下面修改后的代码,现在可以编译和 运行s:
#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) 已移至其派生的 classes。 B 不再尝试引用派生对象。这是一个明显的错误。 weak_ptr 作为后向指针被简单的后向指针所取代(就像原型中的情况一样)。没有泄漏问题,因为派生对象(D1 和 D2)完全拥有该对象。 (在生产代码中,成员类型是共享指针以防止泄漏。)
#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
才能工作。在尚未由 shared_ptr
管理的对象上调用 shared_from_this
实际上是 C++14 中的未定义行为。因此,您将无法从构造函数中调用 shared_from_this
,因为此时该对象不在 shared_ptr
中。
来自 cppreference 的示例...
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
无法有意义地管理自动生命周期的变量,除非出现奇怪的 "deleter"。自动生命周期也可以称为"on the stack".
你的代码有一大堆误解。
首先,引用很少会延长生命周期。将 std::shared_ptr<X>&
存储在 class 中几乎总是一个坏主意;它不会延长任何东西的寿命,更不用说 X
:甚至 shared_ptr
的寿命也不会延长。
B( shared_from_this() )
shared_from_this()
创建了一个 shared_ptr<T>
,而不是 shared_ptr<T>&
,将它传递给 B
的构造函数并期待一个引用是无稽之谈。这个编译完全是 MSVC2015 中的一个缺陷,它默认实现了一个允许将右值转换为引用的扩展。
您在构造函数中调用它的事实也意味着它无法工作。 shared_from_this()
在存储在 enable_shared_from_this
中的 weak_ptr
上调用 .lock()
,一旦 this
实际上由 shared_ptr
管理,
就会被填充]
这几乎总是在创建对象之后才会发生。在你的情况下,它永远不会发生,但即使你在 make_shared
中创建它,它也不会在对象构造函数完成之前发生。
备份,您在第一个代码中存储的 &
是错误的。将 &
存储在一个类型中会为其分配和复制提供引用语义,除非您真正理解它的含义,否则这样做是非常值得怀疑的。 &
应该是 *this
的事实意味着你做错了:复制构造或赋值不会保持不变。
你的底层共享指针设计也出现了类似的问题;重新安装这些共享指针不会自动发生,因此默认情况下它们会做错事。
拥有一个带有指向自身的共享指针的对象是一个糟糕的想法;它是一个不朽的物体,因为它为自己提供了自己的生命。
老实说,我几乎不知道这段代码应该做什么,因为据我所知,它在毫无意义地做有害的事情。也许您正在尝试让成员对象知道拥有它们的对象是谁?并以某种方式将其附加到高级消息传递系统所必需的?
如果您的目标是 broadcaster/listener 系统,那么问题就变成了您的具体要求是什么?你的要求越弱,系统越简单,复杂的代价大。
如果您的大多数广播公司都有少数听众,这些听众的变化并不比广播频率更频繁,那么一个简单的 broadcaster/listener 系统会在广播公司中存储弱指针,并在听众中共享指针令牌会解决你的问题。
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
传递给它。它 returns 又名 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
s 不依赖于签名;所以一个 class 在其一生中听很多 broadcaster
可以有一个 std::vector<token>
.
如果它需要跟踪它的 broadcaster 生命周期,我们可以写一个 on destroy broadcaster:
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);
}
);
忘掉它。
但是如果广播者往往比听众长寿,我只是收听并将其存储在一个向量中。
我开发了一些可以正确编译但在(调试)运行时失败的代码。我正在使用 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;
}
此代码编译并运行s。
但是我在真实代码中的派生对象是共享指针。因此,我将此功能添加到代码中。请注意,我正在使用 enable_shared_from_this
构造获取派生的 class'es 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。
我尝试将共享指针更改为弱指针,但没有成功。有人看到问题了吗?
编辑 1:
根据@pat 的观察,shared_from_this()
不能从构造函数调用,请参阅下面修改后的代码,现在可以编译和 运行s:
#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) 已移至其派生的 classes。 B 不再尝试引用派生对象。这是一个明显的错误。 weak_ptr 作为后向指针被简单的后向指针所取代(就像原型中的情况一样)。没有泄漏问题,因为派生对象(D1 和 D2)完全拥有该对象。 (在生产代码中,成员类型是共享指针以防止泄漏。)
#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
才能工作。在尚未由 shared_ptr
管理的对象上调用 shared_from_this
实际上是 C++14 中的未定义行为。因此,您将无法从构造函数中调用 shared_from_this
,因为此时该对象不在 shared_ptr
中。
来自 cppreference 的示例...
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
无法有意义地管理自动生命周期的变量,除非出现奇怪的 "deleter"。自动生命周期也可以称为"on the stack".
你的代码有一大堆误解。
首先,引用很少会延长生命周期。将 std::shared_ptr<X>&
存储在 class 中几乎总是一个坏主意;它不会延长任何东西的寿命,更不用说 X
:甚至 shared_ptr
的寿命也不会延长。
B( shared_from_this() )
shared_from_this()
创建了一个 shared_ptr<T>
,而不是 shared_ptr<T>&
,将它传递给 B
的构造函数并期待一个引用是无稽之谈。这个编译完全是 MSVC2015 中的一个缺陷,它默认实现了一个允许将右值转换为引用的扩展。
您在构造函数中调用它的事实也意味着它无法工作。 shared_from_this()
在存储在 enable_shared_from_this
中的 weak_ptr
上调用 .lock()
,一旦 this
实际上由 shared_ptr
管理,
这几乎总是在创建对象之后才会发生。在你的情况下,它永远不会发生,但即使你在 make_shared
中创建它,它也不会在对象构造函数完成之前发生。
备份,您在第一个代码中存储的 &
是错误的。将 &
存储在一个类型中会为其分配和复制提供引用语义,除非您真正理解它的含义,否则这样做是非常值得怀疑的。 &
应该是 *this
的事实意味着你做错了:复制构造或赋值不会保持不变。
你的底层共享指针设计也出现了类似的问题;重新安装这些共享指针不会自动发生,因此默认情况下它们会做错事。
拥有一个带有指向自身的共享指针的对象是一个糟糕的想法;它是一个不朽的物体,因为它为自己提供了自己的生命。
老实说,我几乎不知道这段代码应该做什么,因为据我所知,它在毫无意义地做有害的事情。也许您正在尝试让成员对象知道拥有它们的对象是谁?并以某种方式将其附加到高级消息传递系统所必需的?
如果您的目标是 broadcaster/listener 系统,那么问题就变成了您的具体要求是什么?你的要求越弱,系统越简单,复杂的代价大。
如果您的大多数广播公司都有少数听众,这些听众的变化并不比广播频率更频繁,那么一个简单的 broadcaster/listener 系统会在广播公司中存储弱指针,并在听众中共享指针令牌会解决你的问题。
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
传递给它。它 returns 又名 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
s 不依赖于签名;所以一个 class 在其一生中听很多 broadcaster
可以有一个 std::vector<token>
.
如果它需要跟踪它的 broadcaster 生命周期,我们可以写一个 on destroy broadcaster:
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);
}
);
忘掉它。
但是如果广播者往往比听众长寿,我只是收听并将其存储在一个向量中。