Search code examples
c++smart-pointersmove-semantics

Will moving from released pointers leak memory?


I have the following code:

std::unique_ptr<T> first = Get();
…
T* ptr_to_class_member = GetPtr(obj);
*ptr_to_class_member = std::move(*first.release());

Will this behave as expected with no copies, 1 move and without memory leak?


Solution

  • *ptr_to_class_member = std::move(*first.release());
    

    just calls the move assignment operator of T with the object pointed to by first as argument. This may properly transfer some data, but delete is not called or the object so neither T::~T is executed nor does the memory of the object get freed.

    In the example of T = std::string this would result in the backing storage of the string object properly being transfered from the rhs to the lhs of the move assignment, but dynamically allocated memory of size sizeof(std::string) would still be leaked.

    For some classes the lack of a destructor invocation for the object could result in additional trouble, since move assignment simply needs to leave the rhs in an unspecified, but valid state which could still require freeing of additional resources.

    You need to do

    *ptr_to_class_member = std::move(*first);
    first.reset();
    

    in order to prevent memory leaks.


    To show what's going wrong here, the following code implements prints for memory (de)allocation and special member functions:

    #include <iostream>
    #include <memory>
    #include <new>
    #include <utility>
    
    
    struct TestObject
    {
        TestObject()
        {
            std::cout << "TestObject::TestObject() : " << this << '\n';
        }
    
        TestObject(TestObject&& other)
        {
            std::cout << "TestObject::TestObject(TestObject&&) : " << this << ", " << &other << '\n';
        }
    
        TestObject& operator=(TestObject&& other)
        {
            std::cout << "TestObject::operator=(TestObject&&) : " << this << ", " << &other << '\n';
            return *this;
        }
    
        ~TestObject()
        {
            std::cout << "TestObject::~TestObject() : " << this << '\n';
        }
    
        void* operator new(size_t size)
        {
            void* const result =  ::operator new(size);
            std::cout << "memory allocated for TestObject: " << result << '\n';
            return result;
        }
    
        void operator delete(void* mem)
        {
            std::cout << "memory of TestObject deallocated: " << mem << '\n';
            ::operator delete(mem);
        }
    };
    
    
    template<class Free>
    void Test(Free free, char const* testName)
    {
        std::cout << testName << " begin -------------------------------------------\n";
    
        {
            auto ptr = std::make_unique<TestObject>();
            std::cout << "object creation done\n";
            free(ptr);
        }
    
        std::cout << testName << " end ---------------------------------------------\n";
    }
    
    int main()
    {
        TestObject lhs;
    
        Test([&lhs](std::unique_ptr<TestObject>& ptr)
            {
                lhs = std::move(*ptr);
                ptr.reset();
            }, "good");
        Test([&lhs](std::unique_ptr<TestObject>& ptr)
            {
                lhs = std::move(*ptr.release());
            }, "bad");
    
    }
    

    Possible output:

    TestObject::TestObject() : 0000009857AFF994
    good begin -------------------------------------------
    memory allocated for TestObject: 000001C1D5715EF0
    TestObject::TestObject() : 000001C1D5715EF0
    object creation done
    TestObject::operator=(TestObject&&) : 0000009857AFF994, 000001C1D5715EF0
    TestObject::~TestObject() : 000001C1D5715EF0
    memory of TestObject deallocated: 000001C1D5715EF0
    good end ---------------------------------------------
    bad begin -------------------------------------------
    memory allocated for TestObject: 000001C1D5715EF0
    TestObject::TestObject() : 000001C1D5715EF0
    object creation done
    TestObject::operator=(TestObject&&) : 0000009857AFF994, 000001C1D5715EF0
    bad end ---------------------------------------------
    TestObject::~TestObject() : 0000009857AFF994
    

    You can clearly see the destructor call and deallocation missing in the second case, which is the one matching the code you're asking about.