Ownership means responsibility for keeping a resource alive and eventually releasing it. Prefer ordinary values such as strings, vectors and class members when they model your problem. Dynamic allocation is useful when an object's lifetime must be independent of one scope, its address must stay stable while handles move, or a runtime-selected implementation is needed.
Smart pointers are owning objects that apply RAII to dynamically allocated resources. They reduce manual cleanup, but they do not make every pointer access valid. You still need a clear lifetime plan for references, raw pointers and views obtained from the owned object.
Choose one owner unless sharing is required
std::unique_ptr<T> represents exclusive ownership. It cannot be copied, but it can transfer ownership through a move. std::shared_ptr<T> represents shared ownership through a control block: the object is destroyed when the last shared owner releases it. std::weak_ptr<T> observes such a control block without extending the object's lifetime.
Use std::make_unique or std::make_shared to create these owners. Avoid building two independent shared pointers from the same raw pointer; each would believe it must delete the object. A raw pointer returned by get() is a borrowed access path, not an additional owner.
Transfer a job and observe configuration
Save as ownership.cpp:
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
struct Job {
std::string name;
void run() const {
std::cout << "Running: " << name << '\n';
}
};
void inspect(const Job& job) {
std::cout << "Queued: " << job.name << '\n';
}
int main() {
auto pending = std::make_unique<Job>(Job{"index"});
inspect(*pending);
std::vector<std::unique_ptr<Job>> queue;
queue.push_back(std::move(pending));
std::cout << std::boolalpha << "Transferred: " << (pending == nullptr) << '\n';
queue.front()->run();
std::weak_ptr<const std::string> observer;
{
auto config = std::make_shared<const std::string>("local");
auto worker_config = config;
observer = worker_config;
if (auto live = observer.lock()) {
std::cout << "Config: " << *live << '\n';
}
}
std::cout << "Expired: " << observer.expired() << '\n';
}
clang++ -std=c++20 -Wall -Wextra -Wpedantic -g ownership.cpp -o ownership
./ownership
Expected output:
Queued: index
Transferred: true
Running: index
Config: local
Expired: true
The vector owns a unique pointer, which owns a job. Moving the handle into the vector leaves pending empty; that guarantee belongs specifically to unique_ptr's move behavior. The job itself remains at the same allocated address even if the vector later relocates its handles. Erasing its owning handle would destroy the job unless ownership had first moved elsewhere.
What moving actually means
std::move is a cast that permits move-aware operations to treat an expression as a resource source. It does not itself transfer storage. The selected constructor or assignment operator determines what happens. Moving a const object often selects copying because ordinary move constructors need a non-const source.
Many moved-from standard-library objects remain valid but have an unspecified state. You can destroy them or assign a new value; other operations must satisfy their documented preconditions. Do not assume every moved-from string is empty. Returning a local value normally lets the compiler use copy elision or implicit moving; writing return std::move(local) can interfere with elision.
Sharing, observation and cycles
Two shared owners can keep one object alive, but shared ownership adds bookkeeping and makes destruction timing depend on the final owner. It does not make the pointed-to object's fields safe for concurrent mutation. Distinct shared handles can manage their ownership counts concurrently; reading and writing the same handle object still needs the appropriate synchronization.
Shared ownership cycles can keep every participant alive indefinitely. Model a back-reference or observer with a weak pointer when it should not own the target. Use lock() to obtain a temporary shared owner and then test it. Checking expired() and later using a separate raw pointer creates a gap in which the last owner could disappear. The final expired() call above only reports state in a single-threaded demonstration.
Pass a const T& when a function only observes a required object, a T* when absence is meaningful, and a unique pointer by value when a function takes ownership. A shared pointer parameter should communicate actual participation in shared lifetime management.
Practice and solution guidance
- Add an
enqueuefunction that takes ownership of a job. Guidance: acceptstd::unique_ptr<Job>by value, move it into the queue, and define whether null jobs are rejected. - Keep a weak observer after every shared owner disappears. Guidance: call
lock()and check that it returns an empty shared pointer before accessing anything. - Draw ownership arrows for a parent and child. Guidance: let the parent own the child and make the child's back-reference non-owning; explain which owner guarantees its validity.

