This is my learning notes from the course - Mastering C++ Standard Library Features.
Creating Movable Classes
- Why your classes should be movable
- Emplacement
- How to write classes that benefit from movable semantics
- The “
rule of five
“ and “rule of zero
“ - Example: Implementing barebones movable and move-aware
std::vector
Standard Library Support for Movable Types
- The benefits of making your classes move-aware
- Standard Library utilities that work with move-aware user-defined types
Why Movable Types?
- Writing classes that support move operations can greatly increase performance, safety, and expressiveness of your code
- The Standard Library provides many utilities that make use of
move semantics
where possible, even for user-defined types
Almost every container in the Standard Library is “move-aware
“. This means that it will use move operations (if available) when dealing with its items.
Let’s begin by looking at std::vector
.
Assuming that foo
is a class that supports move semantics, we can see that vector
will try to move whenever possible.
1 |
|
The Concept of Emplacement
Containers allow us to go one step further, and sometimes allow us to entirely avoid the move. This can happen when an item is being constructed “in place“ inside the container.
This operation is called “emplacement“ and is supported by most Standard Library containers.
1 | struct bar |
Emplacement works by perfectly-forwarding
all arguments passed to emplace_back
to the constructor of a bar
instance being created directly in the target memory location inside the vector
.
Note that writing move-aware classes is very important even if emplacement exists. Consider the case where a “wrapper” class is going to be emplaced.
1 | struct bar_wrapper |
In the code above, the move operation for bar
cannot be avoided as it happens while constructing bar_wrapper
. This example shows that having support for move operations is beneficial even when trying to maximize the use of emplace_back
and similar methods.
Other Containers
- All the commonly used Standard Library containers are
move-aware
and support emplacement - Examples:
std::vector
,std::map
,std::set
,std::list
Utility Functions and Classes
- Many utility functions and classes support or are defined in terms of
move semantics
- Common utilities:
std::swap
andstd::exchange
- Wrappers:
std::pair
andstd::tuple
- Algorithms:
std::move_iterator
std::swap
The std::swap
function is a very versatile way of swapping two objects. Users can define their own swap
specializations that will be found through ADL, but std::swap
is always a valid fallback.
Before C++11, std::swap
was typically defined as follows:
1 |
|
This was incredibly expensive for a lot of types, which led container implementations to provide their own fast swap
specializations.
In C++11 and later, swap
is defined in terms of move semantics
:
1 | template <typename T> |
As you can see, no copies are executed when using the default implementation of std::swap
!!
This is great both for library users and developers: as long as you provide sensible move operations for your types, std::swap
will be efficient.
std::exchange
Since C++14, an utility that is similar to swap
has been introduced in the Standard Library.
1 | template <typename T, typename U = T> // U is forward to T |
std::exchange
replaces obj
with new_value
, while returning the old value of obj
. It relies on T
and U
supporting move operations.
Here’s a possible implementation:
1 | template <typename T, typename U = T> |
If both T
and U
implement efficient move operations, std::exchange
is a very cheap way of modeling “replacement where the previous value is interesting”.
The most common use case for std::exchange
is implementing move operations for user-defined types.
Here’s a barebones unique pointer implementation using std::exchange
:
1 | class barebones_uptr |
Let’s recall the example of my_unique_ptr:
1 | // Ownership transfer |
And then, take a close look at the implementation with std::exchange
:
1 | barebones_uptr(barebones_uptr&& rhs) : _p{std::exchange(rhs._p, nullptr)} |
As you can see, the first thing we do is to store the first value, which is the right-hand side pointer (rhs._p
), to some temporary location in memory, then we assign nullptr
to rhs._p
so that it will not be freed twice. Finally, we assign rhs._p
value to member _p
variable so that the current unique_ptr
will take ownership of the memory location.
With std::exchange
, we can say that in one line without repeating ourselves and this can be useful both for the move constructor and for the move assignment operator.
One way of quickly remembering what std::exchange
is doing is to look at it as a series of operations that are being executed from left to right. If you read this line of code from left to right, you can see that _p
is going to take the value of rhs._p
, and rhs._p
is going to take the value of nullptr
.
General Purpose Wrapper Classes
std::pair
and std::tuple
are general-purpose wrapper classes that bundle together objects of potentially different types.
As std::pair
is less general than std::tuple
, here’s going to only cover tuple
- all observations also apply to pair
.
tuple
instances can be created either explicitly by using its constructor or through std::make_tuple
, which deduces the types of the objects for you.
Moves are used whenever possible, both during construction and assignment.
1 |
|
Items can be retrieved out of tuples and pairs by using std::get
. std::get
allows you to retrieve items by either index or type.
1 | void retrieving_by_index() |
Since C++14, std::get
supports retrieval-by-type if there are no duplicate types. If the tuple
has duplicate types, std::get
will cause the compiler error.
1 | void retrieving_by_type() |
Tuples
can be destructured by using std::tie
. This is particularly useful when a function returns a tuple
.
1 | std::tuple<foo, bar> get_t(); |
Since C++17, std::apply
can be used to invoke a function with the elements of a tuple. Objects will be moved out of the tuple if appropriate.
1 | int add(int x, int y) { return x + y; } |
Similarly, std::make_from_tuple
(since C++17) can be used to construct an object with the elements of a tuple.
1 | struct foobar |
Tuples
are very useful in generic programming or when writing “glue” code. They can also be used for quick prototyping or to avoid boilerplate when a simple wrapper is required.
Additionally, they support lexicographical comparisons which allows easy definition of by-member comparison operators:
1 | struct two_ints |
std::move_iterator
The Standard Library provides a huge number of general-purpose algorithms in the <algorithm>
header. These algorithms mostly operate on “iterator pairs”.
Some new move-aware algorithms have been introduced in C++11:
move
move_backward
But most of the existing algorithms are not move-aware. Fortunately, std::move_iterator
and std::make_move_iterator
have been added to the <iterator>
header.
This allows users to “adapt“ existing iterator so that they move upon dereference.
1 |
|
The example below will copy all the strings with five characters from src
to dst
.
1 | void example_copy() |
By wrapping the src
iterators with std::make_move_iterator
, the objects satisfying the has_five_chars
predicate will be moved instead.
1 | void example_move() |
Summary
- Implemented move operations for your types allows your code to be faster, safer, and more expressive
- Learned that the standard library provides a huge number of move-aware containers and utilities, which make use of move operations where possible
- Studied that the modern C++ libraries follow the steps of the standard library and will strive to provide more-awareness
- Understood that always think about move semantics and move-awareness when creating your own types