Use value_ptr to get value semantics on a heap resource. At the cost of some extra copying, your code will be simpler and easier to reason about.
TL;DR
Use the value_ptr smart-pointer to get value semantics on a heap resource. At the cost of some extra copying, your code will be simpler and easier to reason about.
| Name | unique_ptr | shared_ptr | weak_ptr | value_ptr |
|---|---|---|---|---|
| Ownership | Unique | Shared | ❌ | Unique |
| Copyable | ❌ | ✅ | ✅ | ✅ |
| Movable | ✅ | ✅ | ✅ | ✅ |
| Sharing | ❌ | Reference | ❌ | Value |
| Lifetime | Lexical | Reference-counted | Non-extending | Lexical |
| Semantics | Reference | Reference | Optional-reference | Value |
An implementation of value_ptr can be found on GitHub.
Introduction
With smart-pointers, encoding ownership semantics and managing resources has never been easier. We can find smart-pointers in the standard library for the most common use-cases, however none of these provides value semantics. This article introduces value_ptr, alongside some motivating examples.
(Dumb) Raw Pointers
Raw pointers do not convey any information about a resource's ownership model. Allocation and deallocation must be managed by the programmer, which may lead to bugs like double-delete or memory-leaks.
void* x = createInstance();
int* y = new int();
foo.bar(y);Three lines raise many unanswered questions: Can x be nullptr? Is it managed? Must I delete it? If I delete y before foo, is foo still valid?
unique_ptr
unique_ptr manages the lifetime of a resource by taking sole ownership and binding that to its lexical scope. Copying is not possible, but ownership can be transferred via std::move.
unique_ptr<Widget> createWindow() {
return make_unique<Widget>();
}
int main() {
auto w = createWindow();
// auto w2 = w; // error: unique_ptr has no copy-constructor
auto w3 = move(w);
// w3 now owns the widget, w is empty
}shared_ptr
shared_ptr allows multiple owners by counting references. The copy-constructor increments the counter; the destructor decrements it. When the counter hits zero the resource is disposed.
shared_ptr<Texture> tex = Texture::load("textures/my_texture.png");
thread(doSomethingWithTexture, tex).detach();
thread(doSomethingMoreWithTexture, tex).detach();weak_ptr
If shared_ptr is used in a cycle, the reference counter never hits zero and memory leaks. weak_ptr does not increment the reference counter, breaking the cycle.
struct Node {
string name;
weak_ptr<Node> parent;
vector<shared_ptr<Node>> children;
};Introducing value_ptr
Value semantics make your code easier to reason about because ownership is strictly hierarchical and exclusive. value_ptr enforces those semantics on a copyable heap resource.
How it Works
value_ptrhas exclusive ownership of a resource on the heap.- Assigning one
value_ptrto another creates a newvalue_ptrpointing to its own copy of the resource. - The resource is destroyed when the
value_ptrleaves its lexical scope. - No memory is shared, so
value_ptris inherently thread-safe. - A modern compiler removes most redundant copies.
Example 1 — Recursive Data Types
Recursive types like trees must use pointers in C++ so the memory layout can be computed at compile-time. With value_ptr we keep the simplicity of value semantics:
struct Tree {
string const name;
value_ptr<Tree> left;
value_ptr<Tree> right;
Tree(
string const& name,
value_ptr<Tree> const& left = value_ptr<Tree>{},
value_ptr<Tree> const& right = value_ptr<Tree>{})
: name{name}, left{left}, right{right}
{}
};
int main() {
Tree root = Tree{
"root",
Tree{"l0"},
Tree{"r0"}
};
root.left = root; // copy of root assigned to left
root.right = root; // no cyclic references!
}Example 2 — The PImpl Pattern
value_ptr is a natural fit for PImpl. It gives value semantics for free, so you don't need to hand-write copy constructors:
struct Foo {
int bar() { return ptr->bar(); }
Foo(int x);
// value_ptr gives us value semantics for free
Foo(Foo const&) = default;
Foo& operator=(Foo const&) = default;
~Foo() = default;
struct Pimpl;
value_ptr<Pimpl> ptr;
};struct Foo::Pimpl {
int x;
int bar() { return ++x; }
};
Foo::Foo(int x) : ptr{Foo::Pimpl{x}} {}Implementing value_ptr
value_ptr is similar to unique_ptr but with a different copy-constructor. Whilst copying a unique_ptr is forbidden, copying a value_ptr creates a copy of the resource. We implement it by leveraging unique_ptr and a copy function.
Take a look at the source on GitHub.
Summary
Value semantics are easy to reason about, and are often useful even for heap objects. The C++ standard library does not provide a smart-pointer with value semantics, but C++ has the features to allow us to roll our own.
| Name | unique_ptr | shared_ptr | weak_ptr | value_ptr |
|---|---|---|---|---|
| Ownership | Unique | Shared | ❌ | Unique |
| Copyable | ❌ | ✅ | ✅ | ✅ |
| Movable | ✅ | ✅ | ✅ | ✅ |
| Sharing | ❌ | Reference | ❌ | Value |
| Lifetime | Lexical | Reference-counted | Non-extending | Lexical |
| Semantics | Reference | Reference | Optional-reference | Value |
Related
- valuable — value_ptr for C++ — The open-source
value_ptrimplementation referenced in this article. - Either vs Error-Codes — Another approach to making errors explicit in the C++ type-system.
- Experimenting with Small-Buffer Optimization — More C++ performance experiments with type-erasure and stack allocation.
Related Articles
Either vs Error-Codes: Lifting Errors into the Type-System
Consider using an Either type to handle errors — they lift errors into the type-system and have the same performance characteristics as error-codes.
Experimenting with Small-Buffer Optimization
We implemented SmallFun, an alternative to std::function using fixed-size capture optimization (a form of small-buffer optimization) that is 3–5x faster in benchmarks.
C++ Coroutine TS — It's About Inversion of Control
One motivating example where lazy sequences enable you to separate your concerns and maximize code reuse — without increasing complexity.