Unique_ptr and shared_ptr replace manual [[delete]] because they encode ownership in the type system. A [[std::unique_ptr]] says exactly one owner exists; a [[std::shared_ptr]] says several owners exist. The memory is released when the ownership ends, automatically, at the right moment, and the compiler enforces the rules rather than leaving them to your memory.
unique_ptr: exactly one owner
A std::unique_ptr owns its object and nobody else may own it. It cannot be copied, only moved.
#include
#include
#include
struct Widget {
explicit Widget(std::string name) : name(std::move(name)) {
std::cout << "creating " << this->name << '\n';
}
~Widget() {
std::cout << "destroying " << name << '\n';
}
std::string name;
};
int main() {
{
auto widget = std::make_unique("alpha");
std::cout << "using " << widget->name << '\n';
}
std::cout << "scope ended\n";
return 0;
}The output shows the destruction happening automatically at the end of the scope:
creating alpha
using alpha
destroying alpha
scope endedThere is no delete, and there is no way to forget one.
Ownership moves, it does not copy
Because a unique pointer cannot be copied, the ownership has to be transferred with std::move.
#include
#include
#include
struct Widget {
explicit Widget(std::string name) : name(std::move(name)) {
std::cout << "creating " << name << '\n';
}
~Widget() {
std::cout << "destroying " << name << '\n';
}
std::string name;
};
void consume(std::unique_ptr widget) {
std::cout << "consuming " << widget->name << '\n';
}
int main() {
auto widget = std::make_unique("alpha");
consume(std::move(widget));
std::cout << "widget is empty: " << (widget == nullptr) << '\n';
return 0;
}The output shows the widget being created, handed over, and destroyed when the function's parameter went out of scope:
creating
consuming alpha
destroying alpha
widget is empty: 1After the move, the source pointer is empty, and using it would be a bug the compiler helps you spot.
shared_ptr: several owners
A std::shared_ptr keeps a reference count. The object is destroyed when the last shared pointer referring to it goes away.
#include
#include
#include
struct Widget {
explicit Widget(std::string name) : name(std::move(name)) {
std::cout << "creating " << name << '\n';
}
~Widget() {
std::cout << "destroying " << name << '\n';
}
std::string name;
};
int main() {
auto first = std::make_shared("alpha");
std::cout << "owners: " << first.use_count() << '\n';
{
auto second = first;
std::cout << "owners: " << first.use_count() << '\n';
}
std::cout << "owners: " << first.use_count() << '\n';
return 0;
}The output shows the count rising and falling as pointers come and go:
creating
owners: 1
owners: 2
owners: 1
destroying alphaThe destructor ran when the last owner disappeared, which is at the end of main.
The cost of shared ownership
A shared pointer is larger than a raw pointer and every copy touches an atomic counter. When the same object is shared across threads, that counter becomes a point of contention.
#include
#include
struct Widget {
int value = 0;
};
int main() {
auto shared = std::make_shared();
auto unique = std::make_unique();
std::cout << "sizeof(raw): " << sizeof(Widget*) << '\n';
std::cout << "sizeof(unique_ptr): " << sizeof(std::unique_ptr) << '\n';
std::cout << "sizeof(shared_ptr): " << sizeof(std::shared_ptr) << '\n';
return 0;
}The output shows the sizes on a 64-bit system:
sizeof(raw): 8
sizeof(unique_ptr): 8
sizeof(shared_ptr): 16A unique pointer is exactly the size of a raw pointer, so there is no performance reason to prefer a raw one.
The cycle problem
Shared pointers can form a cycle, and a cycle means nobody is ever the last owner. The memory leaks even though every shared pointer eventually goes out of scope.
#include
#include
struct Node {
std::shared_ptr next;
int value = 0;
};
int main() {
auto first = std::make_shared();
auto second = std::make_shared();
first->next = second;
second->next = first; // the cycle
first.reset();
second.reset();
std::cout << "pointers released, but the nodes are still alive\n";
return 0;
}Neither node is destroyed, because each still holds a shared pointer to the other. The fix is to break the cycle with a std::weak_ptr for the back reference.
#include
#include
struct Node {
std::shared_ptr next;
std::weak_ptr previous;
int value = 0;
};
int main() {
auto first = std::make_shared();
auto second = std::make_shared();
first->next = second;
second->previous = first;
first.reset();
second.reset();
std::cout << "the cycle is broken\n";
return 0;
}A std::weak_ptr does not count towards the ownership total, so it cannot keep an object alive by itself. It is the standard way to express a non-owning link.
Choosing between them
std::unique_ptr— the default. One owner, no overhead, clear ownership.std::shared_ptr— only when several parts of the program genuinely share ownership.std::weak_ptr— for a non-owning reference that must not keep the object alive, and to break cycles.- A raw pointer or reference — for a non-owning reference whose lifetime is guaranteed by something else, such as a member pointing at a member of the same object.
The practical rule: start with std::unique_ptr and change it to a shared pointer only when you can point at a real reason why two things need to own the same object.
Quick reference
std::make_uniqueandstd::make_sharedare the safe ways to create smart pointers.- A unique pointer cannot be copied, only moved, and it deletes its object automatically.
- A shared pointer keeps a count and deletes the object when the count reaches zero.
- A shared pointer is twice the size of a raw pointer and its count is atomic.
- Cycles of shared pointers leak; use a weak pointer for back references.
- Prefer a unique pointer unless you can explain who the second owner is.
One more habit: pass smart pointers by value and move them into the function, rather than passing them by reference and sharing the ownership. Moving keeps a single clear owner at every point, and it makes the moment of transfer explicit in the code.
Try it yourself: write a function that takes a [[std::unique_ptr]] to a widget, prints its name and lets it die at the end. Then write a version that takes a [[std::shared_ptr]] and one that takes a raw pointer, and compare what happens to the object in each case. Finally, build a two-node shared pointer cycle and confirm with the address sanitizer that the memory is never freed.