-
Notifications
You must be signed in to change notification settings - Fork 0
/
220404_default_move_operator_fails.cpp
96 lines (78 loc) · 3.44 KB
/
220404_default_move_operator_fails.cpp
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// This sample code demonstrates a case where default generated move operator is dangerous
// Based on Nicolai Josuttis' "C++ Move Semantics"
// Chapter on "Non-Destructible Moved-From Objects"
// Chapter on "Move Semantics for Containers"
#include <array>
#include <vector>
#include <thread>
#include <chrono>
#include <iostream>
// class that holds an std::array of 10 std::thread's
class Task_array {
public:
// moves a thread to the std::array
template <typename T>
void launch(T op) { threads[num_threads++] = std::thread(std::move(op)); }
// wait for each thread to end
~Task_array() {
std::cout << num_threads << std::endl;
for (int i = 0; i < num_threads; ++i)
threads[i].join();
}
Task_array() = default;
Task_array(Task_array&&) = default;
private:
std::array<std::thread, 10> threads;
int num_threads{0};
};
// class that holds a std::vector of std::thread's
class Task_vector {
public:
// adds a thread to the std::vector
template <typename T>
void launch(T op) { threads.push_back(std::thread(std::move(op))); }
// wait for each thread to end
~Task_vector() {
std::cout << threads.size() << std::endl;
for (auto& t : threads)
t.join();
}
Task_vector() = default;
Task_vector(Task_vector&&) = default;
private:
std::vector<std::thread> threads;
};
int main()
{
try {
using namespace std::chrono_literals;
Task_array ta;
ta.launch([]{ std::this_thread::sleep_for(1s); std::cout << "array:first thread" << std::endl; });
ta.launch([]{ std::this_thread::sleep_for(1.5s); std::cout << "array:second thread" << std::endl; });
// Q1: What happens if we uncomment this line? (Hint: What happens when 'ta' is destructed?)
//Task_array ta_ = std::move(ta);
Task_vector tv;
tv.launch([]{ std::this_thread::sleep_for(2s); std::cout << "vector:first thread" << std::endl; });
tv.launch([]{ std::this_thread::sleep_for(2.5s); std::cout << "vector:second thread" << std::endl; });
// Q2: Why is it okay with vector?
Task_vector tv_ = std::move(tv);
// Q3: At the end of this code block, local objects are destroyed in the reverse order.
// The destructors of Task_array and Task_vector print something to stdout.
// What will be their output?
} catch (std::exception& e) {
std::cout << "exception catched: " << e.what() << std::endl;
}
}
// A1. By the defaulted move constructor, num_threads is copied while each element of the array is moved.
// The moved-from object 'ta' has num_threads equal to 2 but every element of the array is unjoinable
// because they are moved-from. (This is one of the four unjoinable condintions. See "Effective Modern
// C++", item 37.) At the end of the code block, the destructor for the moved-from object 'ta' calls
// join on unjoinable std::thread object. Therefore, program is terminated.
// A2. Unlike the move constructor for std::array where each element is moved, the move constructor for std::vector
// moves the address to the heap where actual data is stored. (std::array stores elements on the stack
// while std::vector stores elements on the heap.) Therefore, the moved-from object 'tv' does not contain
// any threads.
// A3. Destructor of tv_ : 2
// Destructor of tv : 0
// Destructor of ta_(if uncommented) : 2
// Destructor of ta : 2