Last Update:
C++ Smart Pointers Gotchas
Table of Contents
While learning how to use the new C++ Standard, I encountered several intriguing cases with smart pointers. Casting? Array handling? Passing to functions?
Let’s review some common concerns so that we don’t shoot yourself in the foot :)
I created this post back in 2013, and I updated it in 2014 and recently in 2021.
Some predefines
Let us take a simple Test
class with one member field to present further concepts:
struct Test {
Test() { std::cout << "Test::Test\n"; }
~Test() { std::cout << "Test::~Test destructor\n"; }
int val_ { 0 };
};
With the above declaration, we’ll be able to see when it was constructed and destructed.
If we need some more advanced reporting, including move semantics, we could also implement move and copy constructors and assignment operators, but it will be good for now. See more in: Moved or Not Moved - That Is the Question! - C++ Stories
How not to use smart pointers
The critical thing about smart pointers is that they bind the pointer to an object allocated on the heap and then provide precise semantics of who’s the owner of that pointer. That makes it clear who should delete the resource.
In that context, the following use cases are dangerous and wrong:
Test test;
// 1. ptr to a resource on stack!
std::unique_ptr<Text> ptr(&test); // !!!
// 2. ptr of some other ptr
std::unique_ptr<Test> ptr(new Test());
std::unique_ptr<Test> otherPtr(ptr.get()); // !!
Do you see all errors?
- In the first case,
test
lives on the stack, so the compiler knows where it should be deleted. If you pass the pointer into a smart pointer, then at the end of the lifetime of that smart pointer, it will attempt to calldelete
on that pointer! This is undefined behavior! - The second case shows two smart pointers that “shares” a single pointer. When the scope for
otherPtr
ends, it willdelete
the resource, and thenptr
will try todelete
it the second time. This is also undefined behavior. If you want to share a resource, then useshared_ptr
.
Why is auto_ptr
removed in C++17?
I hope it’s an ancient story for you, and you might even not come across auto_ptr
these days (in 2021)… but to have a complete picture, it’s best to mention it here.
auto_ptr
was one of the first types of smart pointers introduced in C++ (in C++98, to be more precise). It was designed to serve as a simple, unique pointer (only one owner, without any reference counter), but people tried to use this also in the form of a shared pointer. None of those functionalities were satisfied by auto_ptr
’s implementation!
A quick example below:
void dangerous(std::auto_ptr<Test> myPtr) {
myPtr->m_value = 11;
}
void AutoPtrTest() {
std::auto_ptr<Test> myTest(new Test());
dangerous(myTest);
myTest->m_value = 10;
}
See the complete example @Compiler Explorer
Try to compile and run this… what happens? It crashes just after we leave the dangerous
procedure! We would assume that in dangerous
, some reference counter for our pointer is incremented, but auto_ptr
has no such thing.
In my case, I got:
Program returned: 139 // segmentation fault
The object is destroyed because when we leave the dangerous
procedure, our pointer gets out of scope, and it is deleted. To make it work, we need to pass a reference to this auto pointer.
Another thing is that we have a limited way of deleting more complicated objects; there is no control over it at all, only standard delete
can be used here.
The C++ Committee deprecated auto_ptr
in C++11 - the compiler should emit a warning in that mode. And in C++17, the type is removed. If you try to compile the above example under the std=c++17
flag (or higher), then you’ll get the following warnings or errors:
warning: 'auto_ptr<Test>' is deprecated: use 'std::unique_ptr' instead
What’s more, due to misleading copy semantics, auto_ptr
couldn’t be used in standard containers. So you couldn’t create std::vector<std::auto_ptr<Test>>
. This works fine with new smart pointers, including unique_ptr
.
See more in: 5 ways how unique_ptr enhances resource safety in your code.
Here’s the link to clang tool: clang-tidy - modernize-replace-auto-ptr - it allows you to automatically replaceauto_ptr
withunique_ptr
.
Why unique_ptr
does work well?
Fortunately, in C++11, we got a brand new set of smart pointers! When we change auto_ptr
to std::unique_ptr<Test>
in our previous example, we will get compile (not runtime) error saying that we cannot pass a pointer to another function. And this is the proper behavior.
unique_ptr
is correctly implemented because of move
semantics. We can move (but not copy) ownership from one pointer to another. We also need to be aware of when and where we pass the ownership.
In our example we can use:
dangerous(std::move(myTest));
To move the pointer’s ownership.
That way, dangerous
has the ownership now and will destroy the pointer at the end of its scope.
See the full example:
#include <memory>
#include <iostream>
struct Test {
Test() { std::cout << "Test::Test\n"; }
~Test() { std::cout << "Test::~Test destructor\n"; }
int val_ { 0 };
};
void dangerous(std::unique_ptr<Test> myPtr) {
myPtr->val_ = 11;
std::cout << "dangerous() ends...\n";
}
void uniquePtrTest() {
std::unique_ptr<Test> myTest(new Test());
dangerous(std::move(myTest));
// myTest->val_ = 10; // not valid
std::cout << "after dangerous()\n";
}
int main() {
uniquePtrTest();
}
And play with the code @Compiler Explorer.
How to use arrays with unique_ptr?
First thing to know:
std::unique_ptr<int> p(new int[10]); // will not work!
The above code will compile, but only delete
(and not delete[]
!) will be called when resources are about to be deleted.
How do we ensure that delete[]
is called?
Fortunately unique pointers have a proper partial specialization for arrays and we can write:
std::unique_ptr<int[]> p(new int[10]);
p[0] = 10;
For our particular example:
std::unique_ptr<Test[]> tests(new Test[3]);
// or better:
auto ptr = std::make_unique<Test[]>(3);
And we will get the desired output:
Test::Test
Test::Test
Test::Test
Test::~Test destructor
Test::~Test destructor
Test::~Test destructor
See the code @Compiler Explorer
If you want to pass the address of the first element, you have to use&(pointerToArray[0])
. WritingpointerToArray
will not work.
How to use arrays with shared_ptr
?
The array support for shared_ptr
came after unique_ptr
and it’s finally enabled since C++17. It works similarly to the other smart pointer:
#include <memory>
#include <iostream>
struct Test {
Test() { std::cout << "Test::Test\n"; }
~Test() { std::cout << "Test::~Test destructor\n"; }
int val_ { 0 };
};
int main() {
std::shared_ptr<Test[]> ptr(new Test[3]);
std::cout << "finishing main...\n";
}
See the code @Compiler Explorer
Since C++20 make_shared
is also updated to handle array types:
auto ptr = std::make_shared<Test[]>(3);
(Note, as of October 2021 make_shared
for arrays is only supported by the MSVC compiler).
Before C++17 shared_ptr
didn’t work with arrays. You can use a custom deleter. For example:
std::shared_ptr<Test> sp(new Test[2], [](Test *p) { delete []p;});
Why create shared_ptr
with make_shared
?
Unique pointers provide their features only via wise usage of C++ syntax (using private copy constructor, assignment, etc.); they do not need any additional memory. But with shared_ptr,
we need to associate some reference counter with our object. How to do that efficiently?
When we do:
std::shared_ptr<Test> sp(new Test());
std::shared_ptr<Test> sp2 = std::make_shared<Test>();
We will get the output as expected:
Test::Test
Test::Test
Test::~Test destructor
Test::~Test destructor
So what is the difference? Why not use syntax similar to the creation of unique_ptr
? The answer lies in the allocation process. With the first construct, we need to allocate space for the object and the reference counter. There is only one allocation (using placement new), and the ref counter shares the same memory block as the pointed object.
Above, you can see a picture with local’s view in Visual Studio. Compare the addresses of object data and reference counter block. For the sp2
, we can see that they are very close to each other.
To be sure I got proper results I’ve even asked question on stackoverflow: c++ - make_shared “evidence” vs default construct - Stack Overflow.
In C++14 there is a nice improvement:make_unique
function ! That way creating smart pointers is a bit more ‘unified’. We havemake_shared
andmake_unique
.
Why don’t create shared_ptr
with make_shared
?
While make_shared
is the first choice and should work in 99% of use cases, there’s one thing you should be aware of.
It’s connected with weak_ptr
and shared_ptr
interaction.
Basically, weak_ptr
stores a weak counter
in the control’s block of the shared pointer. There might be a case where the reference counter for a shared pointer is zero, but the block cannot be deallocated because there might still be weak references.
If you use make_shared
, the object is also allocated in the same memory block as the control block, and thus the memory for your object won’t be deallocated.
This is a scarce situation and might not even harm you in any way (destructors are still called), but it’s good to be aware of this fact.
See the full explanation with examples here: How a weak_ptr might prevent full memory cleanup of managed object - C++ Stories.
How to pass smart pointers to functions?
You can pass a smart pointer to a function; it’s no big deal… but you need to ask one question:
Do I need to give the ownership or just the pointer to the object?
If you need to operate on the object itself, and don’t change the pointer, change the ownership, etc… then pass the pointer for “observation”:
Like
void importantFunction(Test* ptr) { // or a reference
ptr->val_ = 10;
}
auto ptr = make_unique<Test>();
importantFunction(ptr.get());
It’s also consistent with the following recommendation from C++ Core Guidelines:
F.7: For general use, take T* or T& arguments rather than smart pointers
How about cases where you want to change the pointer itself?
In that case you pass a reference:
void importantFunction(std::unique_ptr<Test>& ptr) {
ptr.reset(nullptr);
}
auto ptr = make_unique<Test>();
importantFunction(ptr);
// ptr might be null
See the following C++ Core Guideline:
R.33: Take a
unique_ptr<widget>&
parameter to express that a function reseats the widget.
There are also other options:
- Pass
unique_ptr
by value - this is called sink function, and it transfers the ownership of the pointer. See my other post on that: Modernize: Sink Functions. - Pass
shared_ptr
by value - this shares the pointer, so its reference counter is updated. Such operation is relatively heavy, so use it only if you need ownership inside the function. - Pass
shared_ptr
by reference - similar to theunique_ptr
case, this tells the caller that the function may reset the pointer.
How to cast smart pointers?
Let’s take a typical example with a simple inheritance:
class BaseA {
protected:
int a{ 0 };
public:
virtual ~BaseA() { }
void A(int p) { a = p; }
};
class ChildB : public BaseA {
private:
int b{ 0 };
public:
void B(int p) { b = p; }
};
Without a problem, you can create a smart pointer to BaseA
and initialize it with ChildB
:
std::shared_ptr<BaseA> ptrBase = std::make_shared<ChildB>();
ptrBase->A(10);
But how to get a pointer to a ChildB
class from ptrBase
? Although it is not a good practice, sometimes we know it is needed.
You can try this:
ChildB *ptrMan = dynamic_cast<ChildB *>(ptrBase.get());
ptrMan->B(10);
It should work. But, that way, you get a ‘normal’ pointer only! The use_count
for the original ptrBase
is not incremented. You can now observe the object, but you are not the owner.
It is better to use casting functions designed for smart pointers:
std::shared_ptr<ChildB> ptrChild = std::dynamic_pointer_cast<ChildB>(ptrBase);
if (ptrChild) {
ptrChild->B(20);
std::cout << "use count A: " << ptrBase.use_count() << std::endl;
std::cout << "use count B: " << ptrChild.use_count() << std::endl;
}
by using std::dynamic_pointer_cast you get a correct shared pointer. Now you are also the owner. Use count for ptrBase
and ptrChild
is ‘2’ in this case.
What about unique_ptr
casting?
In the previous example, you got a copy of the original pointer. But unique_ptr
cannot have copies… so it is no sense to provide casting functions. If you need a casted pointer for observation, then you need to do it the old way.
If you're interested in smart pointers - have a look at my handy reference card. It covers everything you need to know about unique_ptr
, shared_ptr
and weak_ptr
, wrapped in a beautiful PDF:
Summary
Smart pointers are handy, but we, as users, also need to be smart :)
We covered a lot in this article! From basic scenarios to managing arrays or even pointer casting. I hope this will give you enough start to explore smart pointers and refactor code.
Back to you
- What are the everyday use cases for smart pointers in your projects?
- Do you know some other tricks?
Share your experience in the comments below the article.