Morpheu5
Morpheu5

Reputation: 2801

Yet Another "pointer being freed was not allocated"

OK, I'm aware of the tons of posts regarding this error, but I couldn't find one with a simple example and a clear explanation of the issue. Disclaimer: I swear this is not an assignment :) I was just playing around with smart pointers, to figure them out, and this happened.

shared_ptr<string> a = make_shared<string>("This is a string.");
shared_ptr<string> b = make_shared<string>("This is b string.");
cout << a.use_count() << " " << b.use_count() << endl;
a.reset(b.get());
cout << a.use_count() << " " << b.use_count() << endl;

After this code, I would expect a to point at b – which is the case – and b getting a use count of 2. Instead, I get the following output:

1 1
1 1

Fair enough, I thought. After all, I'm passing a raw pointer, how the heck is the compiler/runtime supposed to know of its associated control block, right? Right?! (ok, I genuinely expected the compiler to be able to figure this out, but I suppose this is a simple case, and there must be good reasons if this does not happen)

Moving on, I have the following code

cout << a << " " << *a << endl;
cout << b << " " << *b << endl;

which nicely prints out

0x7f8f10403288 This is b string.
0x7f8f10403288 This is b string.

OK, this is exactly what I expected: a now points at b. But then, immediately after, I have a return 0 and I get out of the main(), whereas the output shouts out the following

tests(62775,0x7fff7940d000) malloc: *** error for object 0x7f8f10403288: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug

Now I'm confused: which pointer being freed was not allocated, exactly? My money would be on either a or b, since they point at the same thing, naturally they are going out of scope, thus their delete is called twice on the same pointer. Am I right? Am I missing something? Is there something else I should absolutely know?

EDIT I tried the dumb-pointer equivalent

string* a = new string("This is a string");
string* b = new string("This is b string");

a = b;

cout << *a << endl << *b << endl;

and the code didn't choke this time. So, is there something special happening around smart pointers?

Upvotes: 2

Views: 1143

Answers (5)

ronalchn
ronalchn

Reputation: 12335

You should never get a raw pointer out of a smart pointer and pass it into another smart pointer type like you have done (with a.reset(b.get());).

What is happening:

What has happened is that both "owner groups" of smart pointers a and b are trying to manage the same raw pointer. When you pass a raw pointer to the reset function, the shared_ptr a assumes it is getting an un-owned pointer, so it will manage the lifetime of the raw pointer. When the last shared_ptr in an ownership group is deleted, the associated memory is de-allocated.

The problem is that b deletes the string, then a tries to delete the string again when it is destructed. You should never try to free the same memory twice. Otherwise the behaviour is undefined, and you may get an exception, or you may overwrite memory you shouldn't (because it has been re-allocated) which can cause all sorts of bugs in un-related parts of your program.

Of course, this happens just after the return statement, because that is when the smart pointers a and b go out of scope (on the stack) and get destructed.

How to assign one shared_ptr to another:

To assign the shared pointer, you should simply say:

a = b;

This will automatically de-allocate whatever a was pointing to, and set it to b, but the smart pointers will know that both of them are pointing to the same object, rather than trying to manage the lifetime of the same string independently.

Basically, the operator= (as well as the copy constructor) of a shared pointer creates a new shared pointer belonging to the same group (sharing the same reference count). So the use_count() of both shared pointers will return 2 (because they are aware of each other), and the memory will only be freed when the last shared pointer is destructed.

Upvotes: 7

Peter
Peter

Reputation: 36617

You need to understand that shared_ptr and make_shared are library features, not features of the compiler.

The compiler doesn't examine the context in which pointers they contain are provided - the behaviour of library functions are specified in the standard. In practice, that means the behaviour is hard-coded in source of the library, and is not changed by the compiler depending on context of use.

The get() member simply returns a raw pointer. It doesn't cause the containing object to somehow register that the object may be managed by another shared_ptr.

The reset() member on the other hand assumes it is receiving an unowned pointer, and claims ownership.

Because of this a.reset(b.get()) causes both a and b to believe they own the same pointer and are responsible for its lifetime, with neither being aware of the other claiming ownership. When a is destroyed, it deletes that pointer. So does b. The net effect is deleting the same object twice. That is undefined behaviour.

Use a = b to make a and b jointly share ownership.

Upvotes: 2

Brent Bradburn
Brent Bradburn

Reputation: 54939

"Is there something special happening around smart pointers?" -- Yes! That is what differentiates smart-pointers from raw pointers. Smart pointers do certain special things implicitly and automatically:

  • Automatically share memory with other smart-pointers
  • Keep track and automatically release memory once no-one is referencing it

This is accomplished in the standard library through the magic of class-specific copy operations and destructors. These are special functions that get called -- like magic -- when certain events occur.

Using a=b is the right way to share ownership between two smart-pointers, but it is a bit of a mind-bender. Typical assignment operations in C++ use the approach known as value-type semantics. That means that the entire value of the object being assigned is actually copied from one place to another -- or it least the system makes it appear as such. However for smart-pointers, things are different. Smart-pointers implement the concept of a reference type. That means that assignment only transfers a reference and updates a reference count -- and the two variables will subsequently refer to the same memory object.

Typically, you wouldn't want to call get() on a shared-pointer. It is a low-level function designed for people who are doing messy stuff. get() returns a raw-pointer and relinquishes control to the programmer -- all of the special features of smart-pointers are bypassed. When you use get(), you are basically telling the library that you know what you are doing -- and that you will be careful. If that turns out not to be the case, you get ugly run-time errors such as pointer being freed was not allocated.

Upvotes: 1

Brian Bi
Brian Bi

Reputation: 119382

The pointer owned by b is being freed twice as a and b go out of scope, since a and b both separately have a use count of 1 for that pointer.

Your dumb pointer equivalent doesn't free anything at all, so it doesn't crash, but if you ran it under valgrind, you would see that it leaks.

btw, there really isn't any good way for the library to figure out whether a pointer is already owned by another smart pointer object. (It's not the compiler's job---the compiler just translates the code.) The control block is not necessarily near the object. Even if it were, in the case that it's not there, a segmentation fault could be triggered. Perhaps shared_ptr could maintain a global hash table of owned pointers, but then that would have too much overhead. So instead, you'll have to write code sensibly. Never call .get() and pass the raw pointer to a function that will take ownership.

Upvotes: 3

Benjamin Lindley
Benjamin Lindley

Reputation: 103733

The reset function expects a pointer to a freshly allocated object, not one that is already managed by another shared pointer. So your suspicions are correct. a and b both think they are the sole owners of the pointer. So when they go out of scope, they both call delete on the same pointer in their respective destructors. If you want a to share ownership with b, just use the shared_ptr assignment operator.

a = b;

Upvotes: 5

Related Questions