Recently, a problem occurred for a month and failed to be solved after two modifications. The general scenario is as follows:
A polymorphic Children object is registered with a callback (m_observer in the Base class) that is called right inside the destructor, causing crash.
class Base { // ... protected: std::shared_ptr<Observer> m_observer; } class Children: public Base { Children(): Base() {m_observer->Register(STD ::bind(&Children::callback, this)); } virtual void callback() {}; };Copy the code
The first change is to cancel the callback subscription to the Observable in the base class so that the observable does not exist when the callback is called.
class Base { virtual ~Base() { m_observer->Register(nullptr); // Cancel callback} //... };Copy the code
It was found that every class containing m_Observer would need to do this, which would be a lot of repetitive code and not concise enough, so we considered further optimization and simply unsubscribe the callback subscription in the Observer destructor. The destructor does not need to write any code:
class Base {
virtual ~Base() = default;
// ...
};
Copy the code
As a result, a callback happens during the Children destructor, and the underlying Observable gets the count of m_Observer, so m_Observer doesn’t perform the destructor, and the callback doesn’t exist, causing the crash.
An Observable holds a weak pointer to an Observer to implement weak callbacks. In other words, an Observable checks whether the Observer is alive by raising the weak pointer to a strong pointer. If the Observer is alive, it calls its registered callback function; otherwise, it does not. The ideal is nice, but the fact is that the combined mode breaks this principle, because the combined mode only holds the Observer. When the outer object is destructed, a callback occurs, which is equivalent to the Observer being stolen by Observable, and the callback outer object no longer exists. If you inherit the Observer interface, this problem does not exist because the object is a full Observer.
This is also the advantage of inheriting an Observer interface. Objects are complete, and as long as a strong Observer pointer is obtained, the object is guaranteed to be alive.
Of course, I am not writing this to advocate what multiple inheritance, critical combination mode, but both sides have application scenarios only, can not generalize, draw the conclusion that combination is better than inheritance.
The problem still needs to be solved. The original solution of the callback, is it possible to solve the problem by manually untying the callback in Base?
class Base { virtual ~Base() { m_observer->Register(nullptr); // Cancel callback} //... };Copy the code
Analyze:
- If the callback occurs before m_Observer ->Register(NULlPTR), then the Register interface is locked and will wait for the m_Observer ->Register(NULlPTR) statement to be executed while the object is alive.
- It is also safe to assume that the callback will occur after m_Observer ->Register(NULlPTR), since the callback is cancelled, so no callback will occur.
In fact, it still crashes. This is a bit of a surprise. Further analysis reveals that the registered callback is a virtual function of the subclass:
class Children: public Base { Children(): Base() {m_observer->Register(STD ::bind(&Children::callback, this)); } virtual void callback() {}; // Virtual function as callback};Copy the code
A callback occurs in case 1 above, calling the subclass’s virtual function callback with the empty address always at the top of the stack:
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 Cause: null pointer dereference x0 0000007deaeb1498 x1 000000000000009f x2 0000007def601a34 x3 0000000000000004 x4 0000000000020002 x5 0000007def601a20 x6 0000000000000000 x7 7f7f7f7f7f7f7f7f x8 0000000000000000 x9 27da922d41dff5aa x10 0000007def6014a0 x11 0000000000000042 x12 0000000000000000 x13 0000000000000000 x14 0000000000000004 x15 0000141f5dfff2d0 x16 0000007e05eddd98 x17 0000007e04b76e6c x18 0000007deeaa8000 x19 0000007def601a20 x20 0000000000000000 x21 0000007deaeb1498 x22 0000000000000004 x23 0000007def601a34 x24 000000000000009f x25 0000007def602020 x26 0000000000000000 x27 0000000000000001 x28 0000007e047da458 x29 0000007def601a10 sp 0000007def601650 lr 0000007e05dc0b8c pc 0000000000000000 backtrace: #00 pc 0000000000000000 <unknown> #01 pc 00000000003cfddc ...Copy the code
Function addresses are empty, and only virtual functions can occur. I wrote a prototype to verify the behavior of case 1:
struct Base { virtual ~Base() { printf("%s\n", __func__); sleep(100); // Ensure that the object is alive during the callback}}; Struct Children: Base {virtual void func() {// The virtual function as a callback puts("virtual func call!" ); } ~Children() override { printf("%s\n", __func__); }}; int main() { Children* c = new Children; std::thread t([&c] { while (true) { c->func(); // Call the subclass's virtual function sleep(1); }}); sleep(5); // (1) delete c; // (2) wait for t.box () in the destructor of the base class; // crash !! return 0; }Copy the code
The call stack is as follows:
#0 0x0000000000000000 in ?? ()
#1 0x0000555555554f19 in <lambda()>::operator()(void) const (__closure=0x555555769e98) at tt.cpp:43
#2 0x0000555555555229 in std::__invoke_impl<void, main()::<lambda()> >(std::__invoke_other, <lambda()> &&) (__f=...) at /usr/include/c++/7/bits/invoke.h:60
#3 0x0000555555555034 in std::__invoke<main()::<lambda()> >(<lambda()> &&) (__fn=...) at /usr/include/c++/7/bits/invoke.h:95
...
Copy the code
Look again at the virtual function table for stage (1) object C:
vtable for 'Children' @ 0x555555756c88 (subobject @ 0x555555769e70):
[0]: 0x55555555564a <Children::~Children()>
[1]: 0x555555555680 <Children::~Children()>
[2]: 0x55555555562e <Children::func()>
Copy the code
In phase (2), the virtual function table for the object is as follows:
vtable for 'Children' @ 0x555555756cb0 (subobject @ 0x555555769e70):
[0]: 0x5555555555ce <Base::~Base()>
[1]: 0x555555555602 <Base::~Base()>
[2]: 0x0
Copy the code
It can be concluded that during the destruction of the base class, the virtual function table of the subclass has been emptied. At this point, the virtual function of the key class is not safe, although the object is still alive, but incomplete. It is safe to release the callback before the destructor by adding an interface.
In the second subject, Effective C++ also points out that virtual functions cannot be called in the construction and destructor. The reason is that there is no polymorphism in virtual functions during this period. Therefore, even if the code complies with the principle, virtual functions cannot be called during the destructor, especially when they are called in multi-threaded scenarios.
Update ing..
It is not elegant to add an interface to release the callback before the destructor, because the subclass rewrites to remember that such an interface needs to be called, so it continues to refactor to achieve the following perfect solution:
Class Base: public STD ::enable_share_from_this<Base> protected: std::shared_ptr<Observer> m_observer; } class Children: public Base { Children(): Base() {Base() {Base() {Base() {Base() {Base(); m_observer->Register(std::bind(&Children::callback, this)); std::weak_ptr<Base> wpBase = enable_from_this(); M_observer ->Register([wpBase] () {STD ::shared_ptr<Base> spBase = wpbase.lock (); If (spBase) {// If (spBase) {// If the object is still alive, then the callback is called, which also guarantees that the object is intact. return std::static_point_cast<Children>(spBase)->callback() } }); } virtual void callback() {}; };Copy the code
All bare Pointers are risky. If encapsulated with a smart pointer, object integrity is guaranteed. In this scenario, simply convert this to a smart pointer, where STD :: enable_SHARE_from_this comes in handy.
Click to follow, the first time to learn about Huawei cloud fresh technology ~