c++valgrindundefined-behavioraddress-sanitizerweak-ptr

Why does this std::weak_ptr apparently prevent freeing memory, and how can I detect this bug?


In my C++ code I have a std::shared_ptr that goes out of scope, which reduces its use count to 0, so the pointed-to object is destroyed. This works fine. However, if I have a std::weak_ptr that points to that std::shared_ptr and stays alive, the pointed-to memory apparently is not actually freed.

Example:

#include <iostream>
#include <memory>

int main() {
    int* my_raw_ptr = nullptr;
    std::weak_ptr<int> my_wp;
    {
        auto my_sp = std::make_shared<int>(42);
        std::cout << "my_sp value: " << *my_sp << std::endl;
        my_raw_ptr = &(*my_sp);

        my_wp = my_sp;  // (1)
    }

    std::cout << "my_raw_ptr value: " << *my_raw_ptr << std::endl;

    return 0;
}

(on Godbolt: https://godbolt.org/z/Kd46fn8zc)

Now, I understand that this code has undefined behaviour (namely, in dereferencing my_raw_ptr after my_sp has gone out of scope). However, my concern is that this error is not caught by any of the tools I tried:

Even more strange: if I remove the std::weak_ptr assignment from the code (i.e. disable the line marked with (1)), all of the three tools correctly detect the bug!

Question: what can I do to make this bug detectable by runtime analysis tools? And, out of curiosity: why does the std::weak_ptr prevent the detection of the bug so well?


Solution

  • First, you must understand how std::make_shared works. When you use it, it allocates memory larger than your object. This extra space contains information needed to handle reference counting and the destruction process.

    Now, both shared_ptr and weak_ptr keep the entire block of memory alive. When all shared_ptrs instances reach the end of their lifetimes, the destructor of the pointed-to object is invoked, but the memory remains unchanged because reference counting is still required to manage weak_ptrs.

    In this case, the object is destroyed, but the memory still exists. An int is a special case since it has a trivial destructor, so you can access this memory without invoking undefined behavior (UB). However, if the pointed-to type has a non-trivial destructor, accessing this memory would cause undefined behavior.

    If you do not use std::make_shared, you will have two blocks of memory (one for reference counting and one for the actual object). When all shared_ptr instances end their lifetimes, the block of memory holding the actual object will be freed. Live demo shows the address sanitizer detecting a memory issue.