In this talk I share lessons learned from implementing a container only supporting movable types. It includes practical advise on making your movable types more efficient, with motivating examples, as well as the state of common standard library types.
13. vector
• If the move constructor can throw it cannot provide the
strong exception guarantee
(i.e. the vector state is unchanged if there’s an error)
14. • If the move constructor can throw it cannot provide the
strong exception guarantee
(i.e. the vector state is unchanged if there’s an error)
x x x x x x
vector
15. • If the move constructor can throw it cannot provide the
strong exception guarantee
(i.e. the vector state is unchanged if there’s an error)
x x x x x
x
Move
vector
16. • If the move constructor can throw it cannot provide the
strong exception guarantee
(i.e. the vector state is unchanged if there’s an error)
x x x x
x x
Move
vector
17. • If the move constructor can throw it cannot provide the
strong exception guarantee
(i.e. the vector state is unchanged if there’s an error)
x x x x
x x
vector
Move fails with exception!
18. • If the move constructor can throw it cannot provide the
strong exception guarantee
(i.e. the vector state is unchanged if there’s an error)
x x x x
x x
How do you restore the state
without risking more failures?
vector
20. struct A {
A(A&&);
};
struct B {
B(B&&) noexcept;
};
vector
NOT nothrow move
constructible
nothrow move
constructible
21. • Traditional (C++98) vector grows by copying.
Still done for types whose move constructor can throw
x x x x x x
vector
22. x x x x x x
x
Copy
• Traditional (C++98) vector grows by copying.
Still done for types whose move constructor can throw
vector
23. x x x x x x
x x
Copy
• Traditional (C++98) vector grows by copying.
Still done for types whose move constructor can throw
vector
24. x x x x x x
x x
• Traditional (C++98) vector grows by copying.
Still done for types whose move constructor can throw
vector
Copy fails with exception.
State unchanged
26. • I built a container and did not want to have to copy
elements
my container
27. • I built a container and did not want to have to copy
elements
• static_assert(
std::is_nothrow_move_constructible_v<T>);
my container
28. • I built a container and did not want to have to copy
elements
• static_assert(
std::is_nothrow_move_constructible_v<T>);
• The static assert failed
my container
31. struct A {
A(A&&) noexcept = default;
std::vector<int> foobar;
};
nothrow movable
error: exception specification of explicitly
defaulted move constructor does not match the
calculated one
A(A&&) noexcept = default;
^
36. struct A {
A(A&&);
int foobar;
};
inline A::A(A&&) = default;
= default
NOT
nothrow move constructible
37. struct A {
A(A&&);
int foobar;
};
inline A::A(A&&) = default;
• The declaration counts (not the definition)
• =default definition will fit even though it deduces
noexcept
= default
38. struct A {
A(A&&) noexcept = default;
int foobar;
};
• “= default”-declaration can be used instead of
static_assert, to ensure nothrow move constructability
= default
40. struct A {
A();
int foobar;
};
inline A::A() : foobar(0) {}
= default
NOT
nothrow default constructible
41. struct A {
A() = default;
int foobar = 0;
};
= default
YES
nothrow default constructible
42. struct A {
A(A&&) noexcept = default;
std::vector<int> foobar;
};
nothrow movable
back to std::vector
43.
44.
45.
46. standard library types
Type
became nothrow move
constructible
std::vector C++17
std::pair C++11
std::tuple
std::string
std::list
std::map
std::set
std::deque
std::forward_list
std::function
47. standard library types
Type
became nothrow move
constructible
std::vector C++17
std::pair C++11
std::tuple C++11
std::string
std::list
std::map
std::set
std::deque
std::forward_list
std::function
48. standard library types
Type
became nothrow move
constructible
std::vector C++17
std::pair C++11
std::tuple C++11
std::string C++11
std::list
std::map
std::set
std::deque
std::forward_list
std::function
49. standard library types
Type
became nothrow move
constructible
std::vector C++17
std::pair C++11
std::tuple C++11
std::string C++11
std::list X
std::map
std::set
std::deque
std::forward_list
std::function
50. standard library types
Type
became nothrow move
constructible
std::vector C++17
std::pair C++11
std::tuple C++11
std::string C++11
std::list X
std::map X
std::set
std::deque
std::forward_list
std::function
51. standard library types
Type
became nothrow move
constructible
std::vector C++17
std::pair C++11
std::tuple C++11
std::string C++11
std::list X
std::map X
std::set X
std::deque X
std::forward_list X
std::function X
52. standard library types
Type
became nothrow move
constructible
std::vector C++17
std::pair C++11
std::tuple C++11
std::string C++11
std::list X
std::map X
std::set X
std::deque X
std::forward_list X
std::function X
}Heap
allocated
sentinel
node
SOO
54. • Implementations are allowed to add noexcept specifiers
• in libc++ (clang) and libstdc++ (GCC) all those containers
and function object are nothrow move constructible
standard library types
55. • Implementations are allowed to add noexcept specifiers
• in libc++ (clang) and libstdc++ (GCC) all those containers
and function object are nothrow move constructible
• The Dinkumware library (msvc) use sentinel nodes in
node-based containers
standard library types
58. sentinel node
List
begin()
end()
• Simplifies edge cases at beginning and end of list
• Empty list still has one heap allocated node
• Move constructing list must
still leave moved-from
container with sentinel node
59. sentinel node
List
begin()
end()
• Simplifies edge cases at beginning and end of list
• Empty list still has one heap allocated node
• Move constructing list must
still leave moved-from
container with sentinel node
• default construction may
throw
60.
61. noexcept
• noexcept move constructors improve performance in
vector
• noexcept move assignment help error handling/
exception safety
63. noexcept
void rotate_log_file() {
auto filename = calculate_filename(...);
file f(filename);
existing_log_file = std::move(f);
}
• perform work that may fail
• commit result with noexcept operation(s)
• move assign or swap()
commit
65. swap
• nothrow move assignable and nothrow swappable are
related attributes
• make it simpler to write fault tolerant code
66. nothrow move assignable
C++14
Type
nothrow move
assignable
nothrow
swappable
std::string ✓* X
std::pair ✓ ✓
std::tuple ✓ ✓
std::vector X X
std::list X X
std::map X X
std::set X X
std::deque X X
std::forward_list X X
std::function X ✓
67. string::operator=
• string assignment was declared noexcept in C++11
• different allocator instances may not support passing
allocations between eachother (but the default one does)
• only libc++ (clang) made it (conditionally) noexcept
• GCC and MSVC did not
68. string::operator=
• Defect report 2063
Contradictory requirements for string move assignment
• ends with Howard Hinnant proposing
using at = allocator_traits<Allocator>;
basic_string& assign(basic_string&& str)
noexcept(
at::propagate_on_container_move_assignment::value
|| at::is_always_equal::value);
69. string::operator=
• Adopted by all containers in C++17
• essence: "if the allocator is move assignable"
using at = allocator_traits<Allocator>;
basic_string& assign(basic_string&& str)
noexcept(
at::propagate_on_container_move_assignment::value
|| at::is_always_equal::value);