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:
-O0 -fsanitize=address -fno-omit-frame-pointer
, the program does not show any error at runtime.MALLOC_CONF=junk:true LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
) does not cause any wrong behaviour at runtime. I would expect that the junk:true
option causes the freed memory to be filled with junk data, making the bug easily visible.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?
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_ptr
s 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_ptr
s.
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.