Move Semantis: Two Nice Properties
I will talk about two nice properties of the move semantic in this post that is not so often mentioned. Containers of the standard template library (STL) can have non-copyable elements. The copy semantic is the fallback for the move semantic. Irritated? I hope so!
Moving Instead of Copying
Do you remember the program packagedTask.cpp from the post Asynchronous callable wrappers? Of course, not. Here, once more.
Moving elements in a container
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 |
// packagedTask.cpp #include <utility> #include <future> #include <iostream> #include <thread> #include <deque> class SumUp{ public: int operator()(int beg, int end){ long long int sum{0}; for (int i= beg; i < end; ++i ) sum += i; return sum; } }; int main(){ std::cout << std::endl; SumUp sumUp1; SumUp sumUp2; SumUp sumUp3; SumUp sumUp4; // define the tasks std::packaged_task<int(int,int)> sumTask1(sumUp1); std::packaged_task<int(int,int)> sumTask2(sumUp2); std::packaged_task<int(int,int)> sumTask3(sumUp3); std::packaged_task<int(int,int)> sumTask4(sumUp4); // get the futures std::future<int> sumResult1= sumTask1.get_future(); std::future<int> sumResult2= sumTask2.get_future(); std::future<int> sumResult3= sumTask3.get_future(); std::future<int> sumResult4= sumTask4.get_future(); // push the tasks on the container std::deque< std::packaged_task<int(int,int)> > allTasks; allTasks.push_back(std::move(sumTask1)); allTasks.push_back(std::move(sumTask2)); allTasks.push_back(std::move(sumTask3)); allTasks.push_back(std::move(sumTask4)); int begin{1}; int increment{2500}; int end= begin + increment; // execute each task in a separate thread while ( not allTasks.empty() ){ std::packaged_task<int(int,int)> myTask= std::move(allTasks.front()); allTasks.pop_front(); std::thread sumThread(std::move(myTask),begin,end); begin= end; end += increment; sumThread.detach(); } // get the results auto sum= sumResult1.get() + sumResult2.get() + sumResult3.get() + sumResult4.get(); std::cout << "sum of 0 .. 10000 = " << sum << std::endl; std::cout << std::endl; } |
I’m not interested in the program that calculates the sum of the numbers from 0 .. 10000 in four threads.
I’m interested in a completely different property of std:::packaged_task. std::packaged_task is not copyable. The reason is simple. The copy constructor and copy assignment operator are set to delete. You can read the details here: cppreference.com.
How is it possible to use std::package_task as an element in a container of the STL? Containers of the STL want to own their elements. Therefore, I move via std::move the std::package_task objects (lines 41 – 44) into the container std::deque. Consequently, I must use the move semantic in lines 52 and 54 because I can not copy the std::package_task.
But that is not the end of the story. If an algorithm of the STL uses, under the hood, no copy semantics, you can apply it to containers with non-copyable elements. You will get a compiler error if the algorithm uses internal copy semantics.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
The algorithm on only moveable elements
In the next example, I make it very simple. I define a simple wrapper MyInt for natural numbers.
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 |
// moveAlgorithm.cpp #include <numeric> #include <algorithm> #include <iostream> #include <utility> #include <vector> class MyInt{ public: MyInt(int i_):i(i_){} // copy semantic MyInt(const MyInt&)= delete; MyInt& operator= (const MyInt&)= delete; // move semantic MyInt(MyInt&&)= default; MyInt& operator= (MyInt&&)= default; int getVal() const { return i; } private: int i; }; int main(){ std::cout << std::endl; std::vector<MyInt> vecMyInt; for (auto i= 1; i <= 10; ++i){ vecMyInt.push_back(std::move(MyInt(i))); } std::for_each(vecMyInt.begin(), vecMyInt.end(), [](MyInt& myInt){ std::cout << myInt.getVal() << " "; }); std::cout << std::endl; auto myInt= MyInt(std::accumulate(vecMyInt.begin(), vecMyInt.end(),MyInt(1),[](MyInt& f, MyInt& s){ return f.getVal() * s.getVal(); })); std::cout << "myInt.getVal(): " << myInt.getVal() << std::endl; std::cout << std::endl; } |
I only declaratively use delete (lines 13 and 14) for the copy semantic and default (lines 17 and 18) for the move semantic. Therefore, the compiler will do the right job for me. I implement only the constructor and the getter getVal (lines 20 – 22). Although my type is non-copyable, I use it in a std::for_each (line 36) and std::accumulate (line 40) algorithm of the STL.
There are no big surprises.
The program will not compile with cl.exe
Unfortunately, the program will not compile with a recent cl.exe Compiler (19.10.24807.0 (x86)). Microsoft std::accumulate versions use, under the hood, copy semantics. This is opposite to recent GCC or clang compilers, which use move semantics. But cl.exe is right. std::accumulate requires that T must be copied, assignable, and copy constructible. This will not hold for MyInt. This issue is still under discussion: https://gcc.gnu.org/onlinedocs/libstdc%2B%2B/ext/lwg-active.html
The copy semantic is a fallback for the move semantic. What does that mean?
Copy Semantics as a Fallback for Move Semantics
If I write an algorithm that internally uses move semantics, I can apply that algorithm to non-copyable types. I have only to change my type MyInt.
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 |
// copyFallbackMove.cpp #include <iostream> #include <type_traits> #include <utility> template <typename T> void swapMove(T& a, T& b){ T tmp(std::move(a)); a= std::move(b); b= std::move(tmp); } class MyInt{ public: MyInt(int i_):i(i_){} // copy semantic MyInt(const MyInt& myInt):i(myInt.getVal()){} MyInt& operator= (const MyInt& myInt){ i= myInt.getVal(); return *this; } int getVal() const { return i; } private: int i; }; int main(){ std::cout << std::endl; MyInt myInt1(1); MyInt myInt2(2); std::cout << std::boolalpha; std::cout << "std::is_trivially_move_constructible<MyInt>::value " << std::is_trivially_move_constructible<MyInt>::value << std::endl; std::cout << "std::is_trivially_move_assignable<MyInt>::value " << std::is_trivially_move_assignable<MyInt>::value << std::endl; std::cout << "myInt1.getVal() :" << myInt1.getVal() << std::endl; std::cout << "myInt2.getVal() :" << myInt2.getVal() << std::endl; swapMove(myInt1,myInt2); std::cout << std::endl; std::cout << "myInt1.getVal() :" << myInt1.getVal() << std::endl; std::cout << "myInt2.getVal() :" << myInt2.getVal() << std::endl; std::cout << std::endl; } |
MyInt has a user-defined copy constructor and copy assignment operator. Therefore, the compiler will not automatically generate a move constructor or move assignment operator. You can read the details here (move semantic) or ask the type traits library (lines 40 and 41) for help. Sadly, my GCC doesn’t support this function of the type traits library. So I have to use a more recent GCC 5.2. The critical point is that instances of MyInt can be used in the function template swapMove (lines 7 – 12), although these instances don’t support move semantics.
The reason is: A rvalue can be bound to
- a constant lvalue reference.
- a non-constant rvalue reference.
The non-constant rvalue reference has a higher priority than the constant lvalue reference. A copy constructor or a copy assignment operator expects its arguments as a constant lvalue reference. A move constructor or a move assignment operator expects its arguments as a non-constant rvalue reference. What sounds a little bit confusing has a lovely property.
Keep the performance in your mind
You can implement functions like swapMove with move semantics in mind. If your data types are not moveable, the functions will also work for only copyable types. The compiler will use the classic copy semantic as a fallback. Therefore, you can comfortably migrate an old C++ codebase to modern C++. Your program is correct in the first iteration and fast in the second.
What’s next?
Writing function temples that can identically forward their arguments, was a ” … a heretofore unsolved problem in C++.“. (Bjarne Stroustrup). This was because since C++11 std::forward we can use perfect forwarding. Read the details in the next post.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!