The problem:
std::vector<int>* get_v() {
return new std::vector<int>(1, 42);
};
To ensure there are no memory leaks, the caller must call delete
on the
returned pointer. I'll use a block scope to illustrate, but in general this issue comes in to play when objects with
different lifetimes need access to the same data.
std::vector<int>* copy;
{
std::vector<int>* orig = get_v();
copy = orig;
// Can't call delete here (unless you copy v)
}
std::cout << *copy[0] << std::endl;
// Have to remember to call delete
delete copy;
A standard approach to the problem is to have an object manage the lifetime of the vector.
class VOwner {
std::vector<int> v_;
public:
VOwner() : v_(1, 42) {}
// Returning a pointer for consistency, but a reference might be a better idea in practice
std::vector<int>* get_v() { return v_; }
};
In this case, we say that the VOwner
object owns the vector. While that removes the need to delete the vector, the user must now ensure that the VOwner object has a longer lifetime than any caller of VOwner::get_v
.
std::vector<int>* copy;
{
VOwner orig;
copy = orig.get_v();
// Oops, vector gets destroyed when orig goes out of scope
}
// Kaboom
std::cout << (*copy)[0] << std::endl;
In many cases the program can be structured so that the owner object
can outlive all of its users. However, in some cases this structure
can be burdensome if lifetime dependencies are hard to unravel, or worse, fundamentally entangled.
Smart pointers allow safe access to objects on the heap without
coupling the access to the client object lifetime.
Unique pointers
Unique pointers (std::unique_ptr
) ensure that an object has exactly one owner. It is possible to change the owner by moving the unique pointer. That allows us to safely implement the original example.
std::unqiue_ptr<vector<int>> get_unique_v() {
return std::make_unique<vector<int>>(1, 42);
}
std::unique_ptr<std::vector<int>> copy;
{
std::unique_ptr<std::vector<int>> orig = get_unique_v();
copy = std::move(orig);
// Now orig can no longer be used to access the vector
}
std::cout << (*copy)[0] std::endl;
// The vector will be freed when copy goes out of scope, no need to manually call delete
Shared pointers
Another common case is when a single object instance needs to be shared by multiple clients. This capability is useful when the instance is expensive to construct, for example. Shared pointers (std::shared_ptr
) can be used to allow clients to mutually guarantee the lifetime of a single instance.
class VShared {
std::shared_ptr<std::vector<int>> v_;
public:
VShared() : v_(std::make_shared<std::vector<int>>(1, 42)) {}
std::shared_ptr<std::vector<int>> get_v() { return v_; }
};
std::shared_ptr<std::vector<int>> copy;
{
VShared orig;
// Assignent operator increments reference count, so the vector
// remains after orig goes after scope
copy = orig.get_v();
}
std::cout << (*copy[0]) << std::endl;
Weak pointers
The above example will persist the vector until the VShared object is
deleted. In some cases, one might prefer to release the object if no
clients are actively using it, while still avoiding creating multiple
instances if one already exists. Weak pointers (std::weak_ptr
) allow safe access to objects managed by std::shared_ptr
without participating in the lifetime of the object.
class VCached {
std::weak_ptr<std::vector<int>> v_;
public:
std::shared_ptr<std::vector<int>> get_v() {
auto candidate = v_.lock();
if (candidate) {
return candidate;
}
auto v = std::make_shared<std::vector<int>>(1, 42);
v_ = v;
return v;
}
};
Note that the nature of weak pointers is that the object they point to
may not exist. The benefit they have over raw pointers is that
attempting to use them after the underlying object has been freed
leads to a controlled failure.