0%

Practical Uses of std::move in C++

This is my learning notes from the course - Mastering C++ Standard Library Features.

Move Semantics in the Standard Library

  • Many classes in the standard library are move aware
  • Some classes represent unique ownership and are move-only
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct foo {};
void pair_and_tuple()
{
// General-purpose utility classes such as `std::pair` and `std::tuple`
// are move-aware.

std::pair<foo, int> p;
auto p_copy = p;
auto p_move = std::move(p);
// http://en.cppreference.com/w/cpp/utility/pair/pair

std::tuple<foo, int, char> t;
auto t_copy = t;
auto t_move = std::move(t);
// http://en.cppreference.com/w/cpp/utility/tuple/tuple

// If the items contained in them have valid move operations, they will be
// properly used when moving the pair/tuple.
}

Every container in C++ is move-aware in one way or another.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void containers()
{
// The entire container can be moved to a destination, or items can be
// moved inside the containers.

std::vector<foo> v;
auto v_copy = v;
auto v_move = std::move(v);

// Moves the temporary inside the vector, instead of copying it.
v.push_back(foo{});

foo f;

// Copies `f` inside the vector.
v.push_back(f);

// Moves `f` inside the vector.
v.push_back(std::move(f));

// http://en.cppreference.com/w/cpp/container/vector/push_back
}

Some classes provided by the Standard Library can only be moved.

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
31
void move_only()
{
// These move only classes represent "unique ownership" over a resource:
// copying them would be nonsensical.

std::thread t{[] { std::cout << "hello!\n"; }}; // std::thread since C++11
// auto t_copy = t; // Does NOT compile.
auto t_move = std::move(t);

// http://en.cppreference.com/w/cpp/thread/thread/thread

// Similarly, `unique_lock` and `unique_ptr` are move-only.

{
std::mutex m;
std::unique_lock<std:::mutex> ul{m};

// auto ul_copy = ul; // Does NOT compile.
auto ul_move = std::move(ul);
}

{
std::unique_ptr<int> up = std::make_unique<int>(1);

// auto up_copy = up; // Does NOT compile.
auto up_move = std::move(up);
}

// Using `std::move` is required when working with these classes, as we need
// to express the intent of "transferring ownership" to the compiler.
}

Avoiding Unnecessary Performance Hits with std::move

  • Moving objects into containers
  • Moving containers instead of copying
  • Accepting rvalue references as function arguments
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
#include <iostream>
#include <map>
#include <string>
#include <utility>
#include <vector>

std::string read_large_file(int index);
std::vector<int> get_multiples(int x);
void consume_multiples(std::map<int, std::vector<int>> m); // passing map by value

void moving_into_containers()
{
// As briefly mentioned earlier, moving into containers can be a huge
// performance win.

// Consider the case where we read large files into an `std::string`,
// then put the string inside an `std::vecor`:

std::vector<std::string> files;

for(int i = 0; i < 10; i++)
{
std::string s = read_large_file(i);
// ... do some processing on `s` ...
files.push_back(s);
}
}

What is wrong with the code above? How can we improve it?

1
2
files.push_back(std::move(s));  // moving instead of copying
// Be careful to NOT to use `s` afterwards.

Moving containers instead of copying:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void moving_containers_instead_of_copying()
{
// `std::string` is a container of characters. The example we looked at above
// applies to any other container but sometimes it is less obvious.

// It is fairly common to have an `std::map` where the value is another container.

std::map<int, std::vector<int>> multiples_of;

for(int i = 0; i < 100; i++)
{
auto i_multiples = get_multiples(i);
multiples_of[i] = i_multiples;
}

consume_multiples(multiples_of);
}

What is wrong with the code above? How can we improve it?

1
2
3
4
5
6
for(int i = 0; i < 100; i++)
{
auto i_multiples = get_multiples(i);
multiples_of[i] = std::move(i_multiples); // moving instead of copying
}
consume_multiples(std::move(multiples_of)); // moving instead of copying

Also, for loop can be reduced to as follow:

1
2
3
4
5
6
for(int i = 0; i < 100; i++)
{
// auto i_multiples = get_multiples(i); // i_multiples is `lvalue`; get_multiples(i) is `rvalue`
// multiples_of[i] = i_multiples; // copy to multiples_of[i] (due to lvalue)
multiples_of[i] = get_multiples(i); // move to multiples_of[i] (due to rvalue)
}

Pass-by-Value Move Idiom

Let’s see an example:

1
2
3
4
5
6
7
8
9
void bar(const std::vector<int>& v);  // pass by const lvalue reference (copy)
void bar(std::vector<int>&& v); // pass by rvalue reference (move)

int main()
{
std::vector<int> v;
bar(v); // `v` is lvalue -> copy
bar(std::move(v)); // `std::move(v)` is rvalue -> move
}

Providing this flexibility in your functions and classes is always a good thing for your users because they will always get optimal performance if they have an lvalue or an rvalue. Unfortunately, it is very cumbersome to provide multiple overloads for everything, this is why the pass-by-value move idiom exists.

Before C++11, it was common to take sink arguments as const& and copy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
#include <utility>

struct person
{
std::string _name;

person(const std::string& name) : _name{name}
{
}

void set_name(const std::string& name)
{
_name = name;
}
};

Since C++11, we can support move operations for sink arguments to prevent unnecessary copies.

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
#include <iostream>
#include <string>
#include <utility>

// Here's a version of `person` with two overloads per constructor/member function.

struct person
{
std::string _name;

person(const std::string& name) : _name{name}
{
}

person(std::string&& name) : _name{std::move(name)}
{
}

void set_name(const std::string& name)
{
_name = name;
}

void set_name(std::string&& name)
{
_name = std::move(name);
}
};

The above code is optimal for the users of person:

  • If they pass an lvalue, it will be copied.
  • If they pass an rvalue, it will be moved.

This quickly gets out of hand, as we need to write 2N overloads for N arguments!

Fortunately, the pass-by-value and move idiom comes to our rescue.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
#include <utility>

struct person
{
std::string _name;

person(std::string name) : _name{std::move(name)}
{
}

void set_name(std::string name)
{
_name = std::move(name);
}
};

By taking sink arguments by value and then unconditionally moving them, we can achieve close-to-optimal behavior:

  • Lvalue => 1 copy + 1 move
  • Rvalue => 2 moves

Since moves are cheap, the extra move is worth not having to provide 2N overloads.

Recap

Avoiding unnecessary performance hits with std::move

  • Sink arguments are objects that are going to be retained/consumed
  • Providing an optimal interface for sink arguments requires 2N overloads for N arguments (all possible combinations of const& and &&)
  • The pass-by-value and move idiom is easy to implement and maintain, and achieves close-to-optimal performance, thanks to move semantics.

Summary

  • Learned that many classes in the standard library are move-aware or move-only
  • Studied that using std::move can provide performance benefits and allows us to work with move-only classes
  • Understood that when taking a sink argument, use the pass-by-value and move idiom