std::unique_ptr: Two common waysPass a pointer created by new to the constructor:
std::unique_ptr<Student> p(new Student("Bob", 2020123123));
Use std::make_unique<T>, and pass the initializers to it.
std::unique_ptr<Student> p1 = std::make_unique<Student>("Bob", 2020123123);
auto p2 = std::make_unique<Student>("Alice", 2020321321);
std::make_unique<T>(args...) perfectly forwards the arguments args... to the constructor of T, as if the object were created by new T(args...).std::make_unique<T> returns a std::unique_ptr<T> to the created object.std::unique_ptrstd::unique_ptr<T> up;
The default constructor of std::unique_ptr<T> initializes up to be a "null pointer".
up is in the state that does not own any object.
std::unique_ptr: Automatic memory managementvoid foo() {
auto pAlice = std::make_unique<Student>("Alice", 2020321321);
// Do something...
if (some_condition()) {
auto pBob = std::make_unique<Studnet>("Bob", 2020123123);
// ...
} // `Student::~Student()` is called for Bob,
// because the lifetime of `pBob` ends.
} // `Student::~Student()` is called for Alice,
// because the lifetime of `pAlice` ends.
A std::unique_ptr automatically calls the destructor once it gets destroyed or assigned a new value.
delete needed!std::unique_ptr: Move-onlyauto p = std::make_unique<std::string>(5, 'c');
std::cout << *p << std::endl; // Prints "ccccc".
auto q = p; // Error. Copy is not allowed.
auto r = std::move(p); // Correct.
// Now the ownership of this string has been transferred to `r`.
std::cout << *r << std::endl; // Prints "ccccc".
if (!p) // true
std::cout << "p is \"null\" now." << std::endl;
std::unique_ptr is not copyable, but only movable.
std::unique_ptr can point to the managed object.std::unique_ptr is the transfer of ownership of the managed object.std::unique_ptr: Move-onlyauto p = std::make_unique<std::string>(5, 'c');
std::cout << *p << std::endl; // Prints "ccccc".
auto q = p; // Error. Copy is not allowed.
auto r = std::move(p); // Correct.
// Now the ownership of this string has been transferred to `r`.
std::cout << *r << std::endl; // Prints "ccccc".
if (!p) // true
std::cout << "p is \"null\" now." << std::endl;
After auto up2 = std::move(up1);, up1 becomes "null". The object that up1 used to manage now belongs to up2.
The assignment up2 = std::move(up1) destroys the object that up2 used to manage, and lets up2 take over the object managed by up1. After that, up1 becomes "null".
You may accidentally write the following code:
// Given that `pWindow` is a `std::unique_ptr<Window>`.
auto p = pWindow; // Oops, attempting to copy a `std::unique_ptr`.
The compiler gives an error, complaining about the use of deleted copy constructor.
What are you going to do?
A. Change it to auto p = std::move(pWindow);.
B. Give up on smart pointers, and switch back to raw pointers.
C. Copy-and-paste the compiler output and ask ChatGPT.
You may accidentally write the following code:
// Given that `pWindow` is a `std::unique_ptr<Window>`.
auto p = pWindow; // Oops, attempting to copy a `std::unique_ptr`.
The compiler gives an error, complaining about the use of deleted copy constructor.
std::unique_ptr is not copyable, but you are copying it. (Direct cause of the error)std::unique_ptr must exclusively manage the pointed-to object. Why would you copy a std::unique_ptr?
p?// Given that `pWindow` is a `std::unique_ptr<Window>`.
auto p = pWindow; // Oops, attempting to copy a `std::unique_ptr`.
What are you going to do with p?
auto p = std::make_unique<Window>(*pWindow);.p to be just an observer, write auto p = pWindow.get();.
pWindow.get() returns a raw pointer to the object, which is of type Window *.p should never interfere in the lifetime of the object. A simple delete p; will cause disaster.// Given that `pWindow` is a `std::unique_ptr<Window>`.
auto p = pWindow; // Oops, attempting to copy a `std::unique_ptr`.
What are you going to do with p?
p to take over the object managed by pWindow, change it to auto p = std::move(pWindow);.
pWindow will no longer own that object.p to be another smart pointer that shares the ownership with pWindow, std::unique_ptr is not suitable here. std::shared_ptr later.std::unique_ptrstruct Window {
// A typical "factory" function.
static std::unique_ptr<Window> create(const Settings &settings) {
auto pW = std::make_unique<Window>(/* some arguments */);
logWindowCreation(pW);
// ...
return pW;
}
};
auto state = Window::create(my_settings);
A temporary is move-constructed from pW, and then is used to move-construct state.
std::unique_ptrup.reset(), up.release(), up1.swap(up2), up1 == up2, etc.
Full list of operations supported on a std::unique_ptr.
std::unique_ptr for array typeBy default, the destructor of std::unique_ptr<T> uses a delete expression to destroy the object it holds.
What happens if std::unique_ptr<T> up(new T[n]);?
std::unique_ptr for array typeBy default, the destructor of std::unique_ptr<T> uses a delete expression to destroy the object it holds.
What happens if std::unique_ptr<T> up(new T[n]);?
new[], but deallocated by delete! Undefined behavior.std::unique_ptr for array typeA template specialization: std::unique_ptr<T[]>.
operator[]. In contrast, it does not support operator* and operator->.delete[] instead of delete to destroy the objects.auto up = std::make_unique<int[]>(n);
std::unique_ptr<int[]> up2(new int[n]{}); // equivalent
for (auto i = 0; i != n; ++i)
std::cout << up[i] << ' ';
std::unique_ptr for array typeA template specialization: std::unique_ptr<T[]>.
- Specially designed to represent pointers that point to a "dynamic array" of objects.
- It has some array-specific operators, e.g. operator[]. In contrast, it does not support operator* and operator->.
- It uses delete[] instead of delete to destroy the objects.
They almost always do a better job. std::unique_ptr<T[]> is seldom needed.
std::unique_ptr is zero-overhead.std::unique_ptr stores nothing more than a raw pointer.
It does nothing more than better copy / move control and automatic object destruction.
Zero-overhead: Using a std::unique_ptr does not cost more time or space than using raw pointers.
[Best practice] Use std::unique_ptr for exclusive-ownership resource management.
std::shared_ptrA std::unique_ptr exclusively owns an object, but sometimes this is not convenient.
struct WindowManager {
void addWindow(const std::unique_ptr<Window> &pW) {
mWindows.push_back(pW); // Error. Attempts to copy a `std::unique_ptr`.
}
private:
std::vector<std::unique_ptr<Window>> mWindows;
};
struct Window {
static std::unique_ptr<Window> create(const Settings &settings) {
auto pW = std::make_unique<Window>(/* some arguments */);
logWindowCreation(pW);
settings.getWindowManager().addWindow(pW);
return pW;
}
};
Design a "shared-pointer" that allows the object it manages to be shared.
When should the object be destroyed?
std::unique_ptr destroys the object it manages when the pointer itself is destroyed.Set a counter that counts how many WindowPtrs are pointing to it:
struct WindowWithCounter {
Window theWindow;
int refCount = 1;
};
When a new object is created by a WindowPtr, set the refCount to 1.
When a WindowPtr is copied, let it point to the same object, and increment the counter.
class WindowPtr {
WindowWithCounter *ptr;
public:
WindowPtr(const WindowPtr &other) : ptr(other.ptr) { ++ptr->refCount; }
};
For copy assignment: the counter of the old object should be decremented.
class WindowPtr {
WindowWithCounter *ptr;
public:
WindowPtr(const WindowPtr &other) : ptr(other.ptr) { ++ptr->refCount; }
WindowPtr &operator=(const WindowPtr &other) {
if (--ptr->refCount == 0)
delete ptr;
ptr = other.ptr;
++ptr->refCount;
return *this;
}
};
* Is this correct?
Self-assignment safe!!!
class WindowPtr {
WindowWithCounter *ptr;
public:
WindowPtr(const WindowPtr &other) : ptr(other.ptr) { ++ptr->refCount; }
WindowPtr &operator=(const WindowPtr &other) {
++other.ptr->refCount;
if (--ptr->refCount == 0)
delete ptr;
ptr = other.ptr;
return *this;
}
};
Destructor: decrement the counter, and destroy the object if the counter reaches zero.
class WindowPtr {
WindowWithCounter *ptr;
public:
WindowPtr(const WindowPtr &other) : ptr(other.ptr) { ++ptr->refCount; }
WindowPtr &operator=(const WindowPtr &other) {
++other.ptr->refCount;
if (--ptr->refCount == 0)
delete ptr;
ptr = other.ptr;
return *this;
}
~WindowPtr() {
if (--ptr->refCount == 0)
delete ptr;
}
};
Move: Just "steal" the object. It is also the transfer of ownership.
class WindowPtr {
WindowWithCounter *ptr;
public:
WindowPtr(WindowPtr &&other) noexcept : ptr(other.ptr) { other.ptr = nullptr; }
WindowPtr &operator=(WindowPtr &&other) noexcept {
if (this != &other) {
if (--ptr->refCount == 0)
delete ptr;
ptr = other.ptr; other.ptr = nullptr;
}
return *this;
}
};
By maintaining a variable that counts how many shared-pointers are pointing to the object, we can know when to destroy the object.
This strategy is adopted by Python.
It can prevent memory leak in many cases, but not all cases!
std::shared_ptrA smart pointer that uses reference counting to manage shared objects.
Create a shared_ptr:
std::shared_ptr<Type> sp2(new Type(args));
auto sp = std::make_shared<Type>(args); // equivalent, but better
For example:
// sp points to a string "cccccccccc".
auto sp = std::make_shared<std::string>(10, 'c');
auto pWindow = std::make_shared<Window>(80, 24, my_settings.mode);
shared_ptrNote: For std::unique_ptr, both of the following ways are ok (since C++17):
auto up = std::make_unique<Type>(args);
std::unique_ptr<Type> up2(new Type(args));
For std::shared_ptr, std::make_shared is preferable to directly using new.
auto sp = std::make_shared<Type>(args); // preferred
std::shared_ptr<Type> sp2(new Type(args)); // ok, but less preferred
Read Effective Modern C++ Item 21. (Note that this book is based on C++14.)
[Best practice] Prefer std::make_shared to directly using new when creating a std::shared_ptr.
* and -> can be used as if it is a raw pointer:
auto sp = std::make_shared<std::string>(10, 'c');
std::cout << *sp << std::endl; // "cccccccccc"
std::cout << sp->size() << std::endl; // "10"
sp.use_count(): The value of the reference counter.
auto sp = std::make_shared<std::string>(10, 'c');
{
auto sp2 = sp;
std::cout << sp.use_count() << std::endl; // 2
} // `sp2` is destroyed, but the managed object is not destroyed.
std::cout << sp.use_count() << std::endl; // 1
Full list of supported operations on std::shared_ptr.
std::shared_ptr is relatively easy to use, since you are free to create many std::shared_ptrs pointing to one object.
However, std::shared_ptr has time and space overhead. Copy of a std::shared_ptr requires maintenance of reference counter.
std::unique_ptr
std::shared_ptr
Does std::shared_ptr prevent memory leak in all cases? Think about what happens in the following code.
struct Node {
int value;
std::shared_ptr<Node> next;
Node(int x, std::shared_ptr<Node> p) : value{x}, next{std::move(p)} {}
};
void foo() {
auto p = std::make_shared<Node>(1, nullptr);
p->next = std::make_shared<Node>(2, p);
p.reset();
}
std::unique_ptr uses is customizable, which also has to be stored in a std::unique_ptr. By default, std::default_delete is used that performs delete ptr; to destroy the object and release the memory. std::unique_ptr often uses some space-saving tricks to store the deleter. If the deleter is "stateless" (e.g. an object with no non-static data members), it may be stored with no unique address so that no extra space is required. std::default_delete belongs to this kind. Therefore, sizeof(std::unique_ptr<T>) is often reasonably equal to sizeof(T *).