Opened 7 years ago
Closed 7 years ago
#11638 closed Bugs (invalid)
stored weak_ptr of track object will prevent the track object memory being freed
Reported by: | Owned by: | Frank Mori Hess | |
---|---|---|---|
Milestone: | To Be Determined | Component: | signals2 |
Version: | Boost 1.56.0 | Severity: | Problem |
Keywords: | signals2 weak_ptr memory leak | Cc: |
Description
boost signals2 will store weak_ptr of a tracked object, if the tracked object is allocated by std::make_shared() or std::allocate_shared(), after it is destructed, the memory occupied by it will not be freed because the weak_ptr is still hold by the signal connection. Only when the signal is re-triggered will the memory being freed.
there's another bug here, after the tracked object is destructed, signals2::connection.connected() will return false, and connection.disconnect() seem does nothing when connected is false, and leave the tracked object memory leaked.
below is some test cases
#include <iostream> // std::cout #include <memory> // std::shared_ptr #include <functional> // std::function #include <boost/signals2.hpp> class TrackObject { public: TrackObject() { std::cout << "construct TrackObject" << std::endl; } ~TrackObject() { std::cout << "destruct TrackObject" << std::endl; } static void* operator new(std::size_t sz) { void* p = ::operator new(sz); std::cout << "custom new for size " << sz << ", got p " << p << std::endl; return p; } static void operator delete(void* p) { std::cout << "custom delete for p " << p << std::endl; return ::operator delete(p); } static void* operator new[](std::size_t sz) { std::cout << "custom new for size " << sz << std::endl; return ::operator new(sz); } private: char data[50]; }; class SignalEmitter { public: using SignalType = boost::signals2::signal<void()>; SignalEmitter() = default; virtual ~SignalEmitter() = default; boost::signals2::connection registerHandler(std::function<void()> func, std::shared_ptr<TrackObject> trackobj) { SignalType::slot_type handler(func); return signal_.connect(handler.track_foreign(trackobj)); } void trigger() { signal_(); } private: SignalType signal_; }; template <class T> struct custom_allocator { typedef T value_type; custom_allocator() noexcept {} template <class U> custom_allocator(const custom_allocator<U>&) noexcept {} T* allocate(std::size_t n) { std::cout << "allocate " << n << " objects, object size is " << sizeof(T) << std::endl; return static_cast<T*>(::operator new(n * sizeof(T))); } void deallocate(T* p, std::size_t n) { std::cout << "deallocate " << n << " objects" << std::endl; ::operator delete(p); } }; int main() { std::cout << "---TestCase1: check wp will hold the memory---" << std::endl; custom_allocator<TrackObject> alloc; { std::weak_ptr<TrackObject> wp4; { auto p4 = std::allocate_shared<TrackObject>(alloc); wp4 = p4; } std::cout << "---TestCase1: expect to free the memory before this line, " << "but wp prevents it from being freed ---" << std::endl; } std::cout << "---TestCase1: the memory is only freed when weak_ptr is out of scope" << ", before this line ---" << std::endl; std::cout << "---TestCase2: check signal2 will prevent memory from being freed---" << std::endl; { SignalEmitter signal_emiiter; boost::signals2::connection conn; { auto trackobj = std::allocate_shared<TrackObject>(alloc); conn = signal_emiiter.registerHandler( []() { std::cout << "signal triggered" << std::endl; }, trackobj); std::cout << "---TestCase2: after registerHandler: conn.connected: " << conn.connected() << std::endl; signal_emiiter.trigger(); } std::cout << "---TestCase2: should free trackobj here, but not---\n"; std::cout << "---TestCase2: connected: " << conn.connected() << std::endl; std::cout << "---TestCase2: connected() shows that it's already disconnected" << std::endl; conn.disconnect(); std::cout << "--TestCase2: so, conn.disconnect() does nothing, " "the memory is still not freed" << std::endl; signal_emiiter.trigger(); std::cout << "---TestCase2: only re-trigger will free memory before this line ---" << std::endl; } std::cout << "---TestCase3: check non-make_shared workaround---" << std::endl; { SignalEmitter signal_emiiter; { std::shared_ptr<TrackObject> trackobj(new TrackObject()); signal_emiiter.registerHandler( [](){ std::cout << "signal triggered" << std::endl; }, trackobj); signal_emiiter.trigger(); } std::cout << "---TestCase3: without use make_shared, memory will be " "freed before this line---" << std::endl; signal_emiiter.trigger(); std::cout << "---TestCase3: re-trigger do nothing ---" << std::endl; } }
Change History (2)
comment:1 by , 7 years ago
comment:2 by , 7 years ago
Resolution: | → invalid |
---|---|
Status: | new → closed |
You may be happier with signals2 from boost 1.59. It deletes slot objects inside the signal (and their tracked weak_ptrs) ASAP after slot disconnection, rather than putting it off for the signal to garbage collect at some later point.
While weak_ptr causing a delay in the deallocation of memory for shared_ptr created with allocate_shared is interesting, it is an issue with allocate_shared, not signals2. My guess is allocate_shared allocates a single block of memory to contain both the pointed-at object and the shared reference count, in order to minimize the number of memory allocations. Thus the memory cannot be freed until both the pointed-at object and the shared reference count are destroyed. Since the weak_ptr has a handle to the shared reference count, it delays the deallocation.
I'm closing this ticket, if there is still a need for an added cleanup API when using signals2 from 1.59, please open a separate ticket for that feature request.
nolock_cleanup_connections_from() is only called by connect() and operator(), I think there's need to add an explicit API to boost::signals2::signal to let the user explicitly cleanup expired connections