How can a memory leak occur when using unique_ptr?
sobes.tech AI
Answer from AI
-
Cyclic dependencies (shared ownership bypassing single-ownership): If two
unique_ptrs own objects that reference each other, and neither is destroyed first, memory will not be freed. This violates theunique_ptrideology as a sole owner. For such cases,shared_ptrwithweak_ptris more suitable.#include <memory> struct A; struct B; struct A { std::unique_ptr<B> ptr_b; A() {} ~A() { /* destructor for A */ } }; struct B { std::unique_ptr<A> ptr_a; // cyclic dependency from A B() {} ~B() { /* destructor for B */ } }; int main() { auto a = std::make_unique<A>(); auto b = std::make_unique<B>(); // Setting mutual references // This will lead to a leak if owner a or b does not destroy its unique_ptr first, // which is impossible here due to mutual ownership a->ptr_b = std::move(b); // When main exits, unique_ptr<A> a will be destroyed, but its destructor // will try to destroy ptr_b. However, if ptr_b owns object B, // and object B contains a unique_ptr<A> that owns the original A, // a cyclic dependency occurs, leading to a leak if not carefully managed. // In this simplified example with make_unique, only a remains in scope // and its destructor is called. Leak occurs if B owns A, and A owns B. // Here, b is moved into a->ptr_b. The unique_ptr<B> b in main is now empty. // When main exits, a is destroyed. Destructor for A is called. // Destructor for unique_ptr<B> ptr_b is called. // Destructor for B is called. // Destructor for unique_ptr<A> ptr_a in B is called. // The ownership cycle problem arises when objects cannot destroy each other // because each owns the other. // A correct example of cyclic dependency: // struct A { std::unique_ptr<B> b; }; // struct B { A* a_raw_ptr; }; // B knows about A but does not own it // With unique_ptr, cyclic ownership is very atypical and hard to create directly // without explicit design errors. A normal cyclic reference does not lead to a leak // with unique_ptr itself if there is no cyclic ownership. // **Real leak scenario with unique_ptr in cyclic contexts:** // It does not occur because of unique_ptr itself, but due to ownership logic. // For example, if you have a structure that *holds* a unique_ptr to another structure, // which in turn *holds* a unique_ptr to the first structure, and you create such objects // with new and assign them to unique_ptr: // auto obj1 = std::make_unique<A>(); // auto obj2 = std::make_unique<B>(); // obj1->ptr_b = std::move(obj2); // obj1 now owns obj2 // obj1->ptr_b->ptr_a = std::move(obj1); // <- PROBLEM HERE: obj2 tries to own obj1, // which already owns obj2. // This will cause a runtime error or an uninitialized unique_ptr, // not a cyclic leak as with shared_ptr. // **Most realistic leak with unique_ptr in dependency contexts:** // If you use a raw pointer inside an object that is pointed to by a unique_ptr, // and this raw pointer points to an object owned by another unique_ptr, and you forget // to reset the raw pointer when the second unique_ptr is destroyed *earlier*. // But this is not a leak of unique_ptr, but a leak of a raw pointer or incorrect memory use. // Let's focus on how the unique_ptr itself can contribute to leaks, // not on design errors outside of it. // The most direct way is an exception. } -
Exceptions during object creation: If an exception occurs during the creation of an object that
unique_ptris to own, or during the constructor of that object after memory allocation (new T()), but before assigning this memory tounique_ptr.#include <memory> class Resource { public: Resource() { // If an exception is thrown here... throw std::runtime_error("Error in Resource constructor"); } ~Resource() { // this destructor will not be called if exception occurs in constructor } }; int main() { Resource* res = nullptr; try { res = new Resource(); // memory allocated // ... but exception occurs before std::unique_ptr takes ownership // std::unique_ptr<Resource> unique_resource(res); // <- code will not reach here std::cout << "This line will not be executed" << std::endl; } catch (const std::exception& e) { std::cerr << "Exception: " << e.what() << std::endl; // `res` points to allocated memory, but it was never assigned to // `unique_ptr`, nor explicitly freed. Leak. // Correct approach: delete res; // in catch block } // Memory leak if delete res; is not called in catch block. // **How to avoid leak with unique_ptr in this case:** // Use std::make_unique try { std::unique_ptr<Resource> safe_resource = std::make_unique<Resource>(); // RAII // If exception occurs in Resource constructor, make_unique // correctly handles freeing the allocated memory. } catch (const std::exception& e) { std::cerr << "Exception handled by make_unique: " << e.what() << std::endl; // No leak } return 0; } -
Incorrect deleter: If
unique_ptris configured with a custom deleter that does not perform its function.#include <memory> #include <iostream> struct MyData { int value; MyData(int v) : value(v) { std::cout << "MyData(" << value << ") created" << std::endl; } ~MyData() { std::cout << "MyData(" << value << ") destroyed" << std::endl; } }; // Custom deleter that *does not* delete memory struct NoOpDeleter { void operator()(MyData* ptr) const { std::cout << "NoOpDeleter called for " << ptr->value << ", but not deleting!" << std::endl; // delete ptr; // <- Forgot to uncomment delete or intentionally do not delete } }; int main() { // Create unique_ptr with custom deleter std::unique_ptr<MyData, NoOpDeleter> data_ptr(new MyData(10)); // When data_ptr goes out of scope, NoOpDeleter is called. // Since NoOpDeleter does not call delete, memory allocated via new MyData(10) // will not be freed. Leak. return 0; // ~unique_ptr() calls NoOpDeleter::operator()(data_ptr.get()), // but memory is not deleted. } -
Calling
.release()without subsequentdelete: Therelease()method relinquishes ownership of the raw pointer. If this raw pointer is not then explicitly deleted (or transferred to another smart pointer), a leak occurs.#include <memory> #include <iostream> struct LeakyResource { LeakyResource() { std::cout << "LeakyResource created" << std::endl; } ~LeakyResource() { std::cout << "LeakyResource destroyed" << std::endl; } }; int main() { std::unique_ptr<LeakyResource> ptr = std::make_unique<LeakyResource>(); LeakyResource* raw_ptr = ptr.release(); // ptr is now null // Ownership transferred to raw_ptr. // Memory pointed to by raw_ptr will not be automatically freed // when main exits. // ... work with raw_ptr ... // Forgot to delete! // delete raw_ptr; // <- Leak if this line is missing std::cout << "ptr is " << (ptr ? "not null" : "null") << std::endl; // Will print "ptr is null" return 0; // raw_ptr still points to allocated memory, but it is not freed }
In all other standard scenarios, unique_ptr, based on RAII (Resource Acquisition Is Initialization), manages memory correctly, freeing it upon scope exit.