0%

Standard Library Support for Movable Types in C++

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <vector>
#include <iostream>

struct foo
{
foo() { std::cout << "foo()\n"; }
~foo() { std::cout << "~foo()\n"; }

foo(const foo&) { std::cout << "foo(const foo&)\n"; }
foo(foo&&) { std::cout << "foo(foo&&)\n"; }
};

void vector_move_awareness()
{
std::vector<foo> v;
foo f0;

// Let's reserve the vector so that internal buffer resizing doesn't produce
// additional uninteresting operations
v.reserve(10);

// Copies `f0`
v.push_back(f0);

// Moves `f0`
v.push_back(std::move(f0)); // convert `f0` from lvalue to rvalue and move

// Moves `foo` temporary
v.push_back(foo{});
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct bar
{
int _x;
bar(int x) : _x{x} { std::cout << "bar(int)\n"; }
bar(const bar&) { std::cout << "bar(const bar&)\n"; }
bar(bar&&) { std::cout << "bar(bar&&)\n"; }
bar() { std::cout << "bar()\n"; }
};

void vector_emplacement()
{
std::vector<bar> v;
v.reserve(10);

// Moves `bar` temporary
v.push_back(bar{42});

// Constructs `bar` instance "in place"
v.emplace_back(42); // will have no `move` or `copy` operation
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct bar_wrapper
{
bar _b;

bar_wrapper(bar b) : _b{std::move(b)}
{

}

void vector_emplacement_and_move()
{
std::vector<bar_wrapper> v;

// Moves `bar` temporary when constructing `bar_wrapper` "in place"
v.emplace_back(bar{42});
}
};

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 and std::exchange
  • Wrappers: std::pair and std::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
2
3
4
5
6
7
8
9
#include <utility>

template <typename T>
void old_swap(T& x, T& y)
{
T tmp{x} // Copy #0
x = y; // Copy #1
y = tmp; // Copy #2
}

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
2
3
4
5
6
7
template <typename T>
void new_swap(T& x, T& y)
{
T tmp{std::move(x)}; // Move #0
x = std::move(y); // Move #1
y = std::move(tmp); // Move #2
}

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
2
template <typename T, typename U = T>  // U is forward to T
T exchange(T& obj, U&& new_value);

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
2
3
4
5
6
7
8
9
template <typename T, typename U = T>
T exchange(T& obj, U&& new_value)
{
// Temporary store old value of `obj`
T old_value = std::move(obj);

// Update `obj` with `new_value`
obj = std::forward<U>(new_value);
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class barebones_uptr
{
public:
barebones_uptr() = default;
barebones_uptr(int* p) : _p{p} { }
~barebones_uptr() { delete _p; }

// Prevent copies
barebones_uptr(const barebones_uptr&) = delete;
barebones_uptr& operator=(const barebones_uptr&) = delete;

barebones_uptr(barebones_uptr&& rhs) : _p{std::exchange(rhs._p, nullptr)}
{
}

barebones_uptr& operator=(barebones_uptr&& rhs)
{
_p = std::exchange(rhs._p, nullptr);
return *this;
}

private:
int* _p{nullptr};
};

Let’s recall the example of my_unique_ptr:

1
2
3
4
5
6
7
8
9
10
11
12
// Ownership transfer
my_unique_ptr(my_unique_ptr&& rhs) : _ptr{rhs._ptr}
{
rhs._ptr = nullptr;
}

my_unique_ptr& operator=(my_unique_ptr&& rhs)
{
_ptr = rhs._ptr;
rhs._ptr = nullptr;
return *this;
}

And then, take a close look at the implementation with std::exchange:

1
2
3
4
5
6
7
8
9
barebones_uptr(barebones_uptr&& rhs) : _p{std::exchange(rhs._p, nullptr)}
{
}

barebones_uptr& operator=(barebones_uptr&& rhs)
{
_p = std::exchange(rhs._p, nullptr);
return *this;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <utility>
#include <cassert>
#include <tuple>

struct foo { };
struct bar { };

void creating_and_assigning()
{
std::tuple<foo, bar, int> t0{{}, {}, 1};
auto t1 = std::make_tuple(foo{}, bar{}, 5);

t0 = t1; // Element-wise copy
t1 = std::move(t0); // Element-wise move
}

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
2
3
4
5
6
7
8
9
10
11
12
13
void retrieving_by_index()
{
auto t = std::make_tuple(foo{}, bar{}, 5);

// Getting lvalue references
foo& i0 = std::get<0>(t);
bar& i1 = std::get<1>(t);
int& i2 = std::get<2>(t);

// Moving out of a tuple
foo m0 = std::move(std::get<0>(t)); // allows you to decide which item you want to move
bar m1 = std::get<1>(std::move(t)); // cast entire tuple to our rvalue reference
}

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
2
3
4
5
6
void retrieving_by_type()
{
auto t = std::make_tuple(foo{}, bar{}, 5);
const auto& i = std::get<int>(t);
assert(i == 5);
}

Tuples can be destructured by using std::tie. This is particularly useful when a function returns a tuple.

1
2
3
4
5
6
7
8
9
10
std::tuple<foo, bar> get_t();

void destructuring()
{
foo f;
bar b;
std::tie(f, b) = get_t();

// The objects will be moved out of the source `tuple` if it is an rvalue.
}

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
2
3
4
5
int add(int x, int y) { return x + y; }
void apply_example()
{
assert(std::apply(add, std::make_tuple(1, 2)) == 3);
}

Similarly, std::make_from_tuple (since C++17) can be used to construct an object with the elements of a tuple.

1
2
3
4
5
6
7
8
9
struct foobar
{
foobar(foo, bar) { }
};

void make_from_tuple_example()
{
auto fb0 = std::make_from_tuple<foobar>(std::make_tuple(foo{}, bar{}));
}

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
2
3
4
5
6
7
8
9
struct two_ints
{
int _a, _b;

bool operator==(const two_ints& rhs) const
{
return std::tie(_a, _b) == std::tie(rhs._a, rhs._b);
}
};

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
2
3
4
5
6
7
8
9
#include <string>
#include <vector>
#include <algorithm>
#include <iterator>

bool has_five_chars(const std::string& s)
{
return s.size() == 5;
}

The example below will copy all the strings with five characters from src to dst.

1
2
3
4
5
6
7
8
9
void example_copy()
{
std::vector<std::string> src{"hello", "world", "aaa"};
std::vector<std::string> dst;

std::copy_if(std::begin(src), std::end(src),
std::back_inserter(dst),
has_five_chars);
}

By wrapping the src iterators with std::make_move_iterator, the objects satisfying the has_five_chars predicate will be moved instead.

1
2
3
4
5
6
7
8
9
10
void example_move()
{
std::vector<std::string> src{"hello", "world", "aaa"};
std::vector<std::string> dst;

std::copy_if(std::make_move_iterator(std::begin(src)),
std::make_move_iterator(std::end(src)),
std::back_inserter(dst),
has_five_chars);
}

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