Card 1 / 9
RAII (Resource Acquisition Is Initialization) ties a resource's lifetime to an object's lifetime: the constructor acquires, the destructor releases. Complete Handle so that constructing one calls acquire() on the Resource (declared above the editable region: it just counts how many handles hold it open) and destroying it calls release().
| scenario | r.open inside the scope |
r.open after the scope |
|---|---|---|
{ Handle h(r); } |
1 |
0 |
{ Handle h(r); throw ...; } caught outside |
1 |
0 β released during stack unwinding |
{ Handle a(r); { Handle b(r); } } |
2, then 1 |
0 |
Constructor body: res_.acquire();. Destructor body: res_.release();. That is all β the compiler guarantees the destructor runs on every exit from the scope: normal fall-through, return, break, or an exception flying past. Copying is deleted because two handles for one acquisition would release it twice. std::unique_ptr, std::lock_guard, std::ofstream and std::vector are all this same idea applied to memory, mutexes, files and buffers.
// RAII (Resource Acquisition Is Initialization): acquire in the constructor,
// release in the destructor. Callers never call release() themselves β the
// destructor runs on EVERY way out of a scope, including exceptions.
class Handle {
Resource& res_;
public:
explicit Handle(Resource& r) : res_(r) {
res_.acquire(); // acquisition happens as part of initialization
}
~Handle() {
res_.release(); // guaranteed to run when the Handle goes out of scope
}
Handle(const Handle&) = delete; // two Handles must not release one acquisition
Handle& operator=(const Handle&) = delete;
};Card 2 / 9
RAII (Resource Acquisition Is Initialization) is not a library feature but a pattern: an object's constructor takes a resource and its destructor gives it back, so cleanup is automatic and exception-safe. Smart pointers are RAII applied to heap memory; the standard library applies the same pattern to mutexes, files and buffers.
| resource | RAII wrapper | manual (non-RAII) equivalent |
|---|---|---|
| heap object | std::unique_ptr<T>, std::shared_ptr<T> |
T* p = new T; ... delete p; |
| mutex | std::lock_guard, std::unique_lock |
m.lock(); ... m.unlock(); |
| file | std::ofstream, std::ifstream |
FILE* f = fopen(...); ... fclose(f); |
| growable buffer | std::vector<T>, std::string |
malloc / free |
The test for "is this RAII?": if you forget the cleanup call, does the resource still get released when the variable goes out of scope? For a raw pointer or FILE* the answer is no β nothing runs at scope exit. For every wrapper in the table the answer is yes, because a destructor does.
Which of these release their resource automatically when the variable goes out of scope β i.e. follow RAII?
std::unique_ptr<T>Correct answerRAII. Its destructor calls delete on the owned object β smart pointers are the textbook example.
std::lock_guard<std::mutex>Correct answerRAII. Locks the mutex in the constructor and unlocks in the destructor, so an early return or exception cannot leave it locked.
T* obtained from new TNot RAII. A raw pointer has no destructor behaviour; if you do not delete it, the object leaks.
std::ofstreamCorrect answerRAII. The stream flushes and closes the file in its destructor β you may call close() early, but you do not have to.
FILE* from fopenNot RAII. It is a C handle; nothing calls fclose for you when the pointer goes out of scope.
std::vector<int>Correct answerRAII. Its heap buffer is freed by the destructor β you never delete[] a vector's storage yourself.
Card 3 / 9
The standard library ships three smart pointers, one per ownership model:
| pointer | ownership | copy? | move? | object destroyed when |
|---|---|---|---|---|
std::unique_ptr<T> |
exactly one owner | no | yes | the owner is destroyed or reset |
std::shared_ptr<T> |
shared, reference-counted | yes (count +1) | yes | the last owner is destroyed or reset |
std::weak_ptr<T> |
none β observes a shared_ptr |
yes | yes | β it never destroys anything; use lock() to get a temporary owner |
Default to unique_ptr; reach for shared_ptr only when ownership genuinely has to be shared, and for weak_ptr when you need to observe a shared object without keeping it alive (caches, back-pointers). std::auto_ptr, the pre-C++11 attempt, was removed in C++17.
Which statements about the standard smart pointers are true?
std::unique_ptr can be moved to transfer ownership.Correct answerTrue. std::move hands the object to another unique_ptr and leaves the source null β the one-owner rule is preserved.
std::unique_ptr can be copied, giving two pointers to the same object.False. Its copy constructor and copy assignment are deleted; two owners would delete the object twice.
std::shared_ptr all share one reference count, and the object is destroyed when that count reaches zero.Correct answerTrue. Every copy points at the same control block; each copy increments the count, each destruction decrements it.
std::weak_ptr keeps the object alive as long as it exists.False. That is precisely what it does not do β it observes without owning, so it never counts toward use_count().
std::weak_ptr you first call lock(), which returns a std::shared_ptr that is empty if the object is already gone.Correct answerTrue. lock() is the only safe way to access the object; the returned shared_ptr keeps it alive while you use it.
Card 4 / 9
A std::unique_ptr owns exactly one heap object; when the pointer dies, so does the object. It can be moved but never copied. Write two functions: make_owned creates an owned int, and take_over moves ownership out of one unique_ptr into the returned one, leaving the source null.
| call | result | state afterwards |
|---|---|---|
auto p = make_owned(42); |
*p == 42 |
p owns the int |
auto q = take_over(p); |
*q == 7 (the same object) |
p == nullptr, q owns it |
take_over(empty) where empty is null |
a null unique_ptr |
empty still null |
return std::make_unique<int>(value); allocates and wraps in one step (no naked new). In take_over, from is a reference, so return from; would try to copy and fail to compile β write return std::move(from);. Moving a unique_ptr nulls the source, which is exactly the "exactly one owner" guarantee at work.
// 1) Create an int on the heap, owned by a unique_ptr. Prefer
// std::make_unique over `new`.
std::unique_ptr<int> make_owned(int value) {
return std::make_unique<int>(value);
}
// 2) Transfer ownership OUT of `from` into the returned pointer.
// Afterwards `from` must be null. unique_ptr cannot be copied β
// only moved.
std::unique_ptr<int> take_over(std::unique_ptr<int>& from) {
return std::move(from); // `from` is a reference, so an explicit move is required
}Card 5 / 9
A std::shared_ptr<T> is one of possibly many owners of the same object; the object is destroyed only when the last owner lets go. Write Worker so that each instance keeps shared ownership of a read-only Config β and keeps working after the code that created the config drops its own handle.
| operation | observable result |
|---|---|
three Workers built from one config |
config.use_count() == 4 β creator + three workers |
config.reset() β the creator lets go |
every worker still reads retries() == 3 |
Worker w{std::move(config)} |
the creator's handle is now empty; w owns alone, count 1 |
Store the handle in a member; taking it by value and moving it in says "this object keeps a share":
explicit Worker(std::shared_ptr<const Config> config)
: config_(std::move(config)) {} // the parameter's share moves into the member
int retries() const { return config_->retries; } // read through the shared handlestd::shared_ptr<const Config> owns the Config but does not allow changing it through this handle β a good fit for settings many workers read. Moving a shared_ptr moves the handle, never the Config: the object stays where make_shared put it, and use_count() goes up by one for each worker that holds a share.
// Settings that many workers read and none of them changes.
struct Config {
int retries;
};
// A Worker keeps SHARED ownership of a Config: as long as any Worker (or the
// creator) still holds a shared_ptr to it, the Config stays alive.
class Worker {
public:
// Take a share of `config` and keep it in `config_`. The parameter is
// taken by value, so a caller can pass a copy (one more owner) or
// std::move its own handle in; either way, move it into the member.
explicit Worker(std::shared_ptr<const Config> config)
: config_(std::move(config)) // the parameter's share now belongs to this Worker
{}
// Read the shared Config through the handle.
int retries() const {
return config_->retries;
}
private:
std::shared_ptr<const Config> config_; // one owner among many
};Card 6 / 9
A std::shared_ptr shares ownership: several pointers may own one object. The object is destroyed β with delete, or a custom deleter given at construction β at the moment the last owner lets go, whether by being destroyed, by reset(), or by being assigned a different pointer.
auto a = std::make_shared<Widget>(); // use_count 1
auto b = a; // use_count 2
a.reset(); // use_count 1 β Widget still alive, b owns it
b = nullptr; // use_count 0 β Widget destroyed herestd::weak_ptr observers do not count. They can outlive the object; they just report expired() afterwards. If you need the object to survive a function call, hold a shared_ptr (for example from weak.lock()) for the duration.
Three std::shared_ptr<T> own the same object, and two std::weak_ptr<T> observe it. When exactly is the T destroyed?
shared_ptr is destroyed, reset(), or assigned another pointer β regardless of the two weak_ptrs.Correct answerCorrect. Only shared_ptrs own; the object dies when the owner count reaches zero. The weak_ptrs simply become expired at that point.
shared_ptrs is destroyed.Incorrect. That would leave the other two owners with a dangling pointer. The count drops from 3 to 2 and the object lives on.
shared_ptrs and both weak_ptrs are gone.Incorrect. Weak pointers do not keep the object alive; they may outlive it and will report expired(). (They do keep the small control block alive, but not the T.)
use_count() drops to 1, because a single owner is the same as unique ownership.Incorrect. One owner is still an owner. The object is destroyed at zero, not at one.
Card 7 / 9
A std::weak_ptr observes an object owned by shared_ptrs without owning it, so it never keeps the object alive. Write still_alive, which reports whether the observed object still exists.
| situation | result |
|---|---|
an owning shared_ptr still exists |
true |
the last owner was destroyed or reset() |
false |
| a copy of the owner is still alive after the original was reset | true |
return !w.expired(); β expired() is true once the owner count has reached zero. Note in the tests that assigning w = owner does not change owner.use_count(): observers do not count.
// A weak_ptr observes an object owned by shared_ptrs without owning it, so
// it never keeps the object alive. Return true if the observed object still
// exists β i.e. at least one shared_ptr owner remains.
bool still_alive(const std::weak_ptr<int>& w) {
return !w.expired(); // expired() == true once the last owner is gone
}Card 8 / 9
You cannot dereference a std::weak_ptr directly β the object might already be gone. lock() promotes it to a std::shared_ptr that owns the object for as long as you hold it (or is empty if the object has expired). Write read_or_default: return the observed value, or -1 if it no longer exists.
| situation | result |
|---|---|
an owner exists, value 99 |
99 |
the last owner was reset() |
-1 |
a default-constructed (never assigned) weak_ptr |
-1 |
if (auto owner = w.lock()) { return *owner; } return -1; β the if both declares the temporary owner and tests it (an empty shared_ptr is false). While owner is in scope the object cannot be destroyed under you, even by another thread; that is the whole point of locking rather than checking expired() and then dereferencing.
// Read the value through a weak_ptr safely. Because the object may already
// be gone, first promote the weak_ptr to a temporary OWNER with lock(); if
// that succeeds, read through it. Return the value, or -1 if it has expired.
int read_or_default(const std::weak_ptr<int>& w) {
if (auto owner = w.lock()) { // lock(): a shared_ptr that owns the object, or empty
return *owner; // safe: `owner` keeps the object alive until this returns
}
return -1;
}Card 9 / 9
Two objects that own each other through std::shared_ptr can never be freed: each keeps the other's count at one even after every outside pointer is gone. The fix is to make one direction non-owning.
struct Node {
std::shared_ptr<Node> child; // parent owns child
std::weak_ptr<Node> parent; // child only OBSERVES parent β no cycle
};parent field type |
after the last outside shared_ptrs go away |
|---|---|
std::shared_ptr<Node> |
both nodes leak: each still has one owner (the other) |
std::weak_ptr<Node> |
parent's count reaches 0 β parent destroyed β its child pointer released β child destroyed |
Rule of thumb: the tree direction (parent β child, container β element) is owning; any back direction (child β parent, observer β subject, cache β entry) is a weak_ptr, and you lock() it when you need to reach the object.
a->other and b->other are both std::shared_ptr<Node>, and they point at each other. The last outside shared_ptr to a and to b is destroyed. What happens?
use_count() == 1 because the other one owns it. Memory leaks.Correct answerCorrect. Reference counting cannot detect a cycle: a is kept alive by b->other, and b by a->other, forever. Making one of the two links a weak_ptr breaks the loop.
Incorrect. shared_ptr counts owners, it does not trace reachability the way a garbage collector does. The two internal owners are enough to keep both alive.
Incorrect. Losing the outside pointer drops each count from 2 to 1, not to 0. Nothing is destroyed.
Incorrect β the opposite problem. Nothing is freed at all; a shared_ptr never deletes while any owner remains.