From 1f9195bf51676759255b1c7e4af1c58f42017843 Mon Sep 17 00:00:00 2001 From: Marcus Mohr <marcus.mohr@lmu.de> Date: Mon, 6 Feb 2023 14:37:50 +0100 Subject: [PATCH] Pimp one example by adding std::move() on rhs of assignment --- notebooks/14_MoveSemantics+RuleOfSix.ipynb | 631 ++++++++++++++++++++- 1 file changed, 630 insertions(+), 1 deletion(-) diff --git a/notebooks/14_MoveSemantics+RuleOfSix.ipynb b/notebooks/14_MoveSemantics+RuleOfSix.ipynb index 3bed6a6..028b8dc 100644 --- a/notebooks/14_MoveSemantics+RuleOfSix.ipynb +++ b/notebooks/14_MoveSemantics+RuleOfSix.ipynb @@ -1 +1,630 @@ -{"metadata":{"kernelspec":{"display_name":"C++17","language":"C++17","name":"xcpp17"},"language_info":{"codemirror_mode":"text/x-c++src","file_extension":".cpp","mimetype":"text/x-c++src","name":"c++","version":"17"}},"nbformat_minor":5,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Move Semantics","metadata":{},"id":"4c244310-2195-4b50-a923-df0b9ba15f7d"},{"cell_type":"markdown","source":"In order to understand what this is about let us take a look at an example from Rainer Grimm's blog [\"Modernes C++ in der Praxis\"](https://www.linux-magazin.de/ausgaben/2012/12/c-11/):","metadata":{},"id":"92b6cf27-b829-4efd-9395-e38f3a330bb4"},{"cell_type":"code","source":"#include <algorithm>\n#include <iostream>\n#include <vector>\n\nclass BigArrayCopy {\n\npublic:\n\n BigArrayCopy( size_t len ) : len_( len ), data_( new int[ len ] ) {}\n\n BigArrayCopy( const BigArrayCopy& other ) : len_( other.len_ ),\n data_( new int[ other.len_ ] ) {\n std::cout << \"copy construction of \" << other.len_ << \" elements\" << std::endl;\n std::copy( other.data_, other.data_ + len_, data_ );\n }\n\n BigArrayCopy& operator=( const BigArrayCopy& other ) {\n\n std::cout << \"copy assignment of \" << other.len_ << \" elements\" << std::endl;\n\n if( this != &other ) {\n\n delete[] data_;\n len_ = other.len_;\n data_ = new int[ len_ ];\n\n std::copy( other.data_, other.data_ + len_, data_ );\n\n }\n\n return *this;\n }\n\n ~BigArrayCopy() {\n if( data_ != nullptr ) {\n delete[] data_;\n }\n }\nprivate:\n\n size_t len_;\n int* data_;\n\n};","metadata":{"trusted":true},"execution_count":2,"outputs":[],"id":"4ffa9b8d-02be-4919-882c-18556a6ded7d"},{"cell_type":"code","source":"int main() {\n \n std::vector< BigArrayCopy > myVec;\n\n BigArrayCopy bArray( 11111111 );\n BigArrayCopy bArray2( bArray );\n\n myVec.push_back( bArray );\n bArray = BigArrayCopy( 22222222 );\n myVec.push_back( BigArrayCopy( 33333333 ) );\n\n}\n\nmain();","metadata":{"trusted":true},"execution_count":3,"outputs":[{"name":"stdout","output_type":"stream","text":"copy construction of 11111111 elements\ncopy construction of 11111111 elements\ncopy assignment of 22222222 elements\ncopy construction of 33333333 elements\ncopy construction of 11111111 elements\n"}],"id":"dd1e7485-7b8e-4728-a217-df899e07d6d8"},{"cell_type":"markdown","source":"Let us analyse the output. Where do the five copies come from?\n<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>\n_____","metadata":{},"id":"333e1d6f-aab5-4c70-859d-db4dafb02981"},{"cell_type":"markdown","source":"We can change that by adding a **move constructor** and a **move assignment operator** to our class:","metadata":{},"id":"6893f7df-99e0-44e9-a79f-8c6e8acba110"},{"cell_type":"code","source":"#include <algorithm>\n#include <iostream>\n#include <vector>\n\nusing std::cout;\nusing std::endl;\nusing std::vector;\n\nclass BigArray {\n\npublic:\n\n BigArray( size_t len ) : len_( len ), data_( new int[ len ] ) {}\n\n BigArray( const BigArray& other ) : len_( other.len_ ),\n data_( new int[ other.len_ ] ) {\n cout << \"copy construction of \" << other.len_ << \" elements\" << endl;\n std::copy( other.data_, other.data_ + len_, data_ );\n }\n\n BigArray& operator=( const BigArray& other ) {\n\n cout << \"copy assignment of \" << other.len_ << \" elements\" << endl;\n\n if( this != &other ) {\n\n delete[] data_;\n len_ = other.len_;\n data_ = new int[ len_ ];\n\n std::copy( other.data_, other.data_ + len_, data_ );\n\n }\n\n return *this;\n }\n\n BigArray( BigArray&& other ) : len_( other.len_ ),\n data_( other.data_ ) {\n cout << \"move construction of \" << other.len_ << \" elements\" << endl;\n other.len_ = 0;\n other.data_ = nullptr;\n }\n\n BigArray& operator=( BigArray&& other ) {\n\n cout << \"move assignment of \" << other.len_ << \" elements\" << endl;\n\n if( this != &other ) {\n\n delete[] data_;\n\n len_ = other.len_;\n data_ = other.data_;\n\n other.len_ = 0;\n other.data_ = nullptr;\n }\n\n return *this;\n }\n\n ~BigArray() {\n if( data_ != nullptr ) {\n delete[] data_;\n }\n }\nprivate:\n\n size_t len_;\n int* data_;\n\n};","metadata":{"trusted":true},"execution_count":1,"outputs":[],"id":"e3784252-4e44-4260-a5a1-d2b92f92e264"},{"cell_type":"code","source":"int main() {\n \n std::vector< BigArray > myVec;\n myVec.reserve(2); // get's rid of the final copy operation, when myVec was reallocated\n\n BigArray bArray( 11111111 );\n BigArray bArray2( bArray );\n\n myVec.push_back( bArray );\n bArray = BigArray( 22222222 );\n myVec.push_back( BigArray( 33333333 ) );\n\n}\n\nmain();","metadata":{"trusted":true},"execution_count":4,"outputs":[{"name":"stdout","output_type":"stream","text":"copy construction of 11111111 elements\ncopy construction of 11111111 elements\nmove assignment of 22222222 elements\nmove construction of 33333333 elements\n"}],"id":"ebbe2374-3d00-4340-98a0-9352ff8ca2cb"},{"cell_type":"markdown","source":"As we can see the assignment and copying of the two temporaries in lines # 10 and 11 now uses our **move semantics**.\n\nBut what is does the **\"&&\"** in the interface of the two methods represent? It is an **r-value reference**.\n","metadata":{},"id":"2fead1ad-cee3-4a0d-ba70-b14066a349b9"},{"cell_type":"markdown","source":"### R-Value References\n\nFirst of all, what are \"r-values\"? Complicated question, short (simplified) answer: stuff that can appear on the right-hand side of an assignment, hence the name (originally). Examples of r-values are:\n\n- temporary objects\n- unnamed objects\n- objects whose address is undeterminable\n\nsuch as\n\n```\nint fourtyTwo = 42;\nstd::string a = std::string( \"rhs is an rvalue\");\nstd::string b = std::string( \"r\" ) + std::string( \"-value\" );\nstd::string c = a + b;\nstd::string d = std::move(b);\n```\n\nThe last line is especially interesting. [std::move](https://en.cppreference.com/w/cpp/utility/move) effectively returns an r-value reference for its argument:\n\n\n> std::move is used to indicate that an object t may be \"moved from\", i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type. ","metadata":{},"id":"a2f2682a-1823-46c4-a444-a23fc192ef17"},{"cell_type":"code","source":"int value = 1;\nint& lRef1 = value;\n// int&& rRef1 = value;\nint&& rRef2 = 1;\nconst int& lRef2 = 1;","metadata":{"trusted":true},"execution_count":7,"outputs":[],"id":"7c569151-43ef-44a8-babc-6928ca17b626"},{"cell_type":"markdown","source":"An r-value can bind to an r-value reference, but also to a constant l-value reference. That's why `BigArrayCopy` worked after all. However, binding to an r-value reference, if possible has **higher precedence**. That's what we need in `BigArray` for the move methods.","metadata":{},"id":"1ad6ad50-160f-49d9-b486-2ac84c1cbdbb"},{"cell_type":"markdown","source":"#### STL Containers","metadata":{},"id":"a0fd8007-04cc-43a5-8c29-07de525a132b"},{"cell_type":"markdown","source":"The containers of the STL support move semantics:","metadata":{},"id":"87780808-08d4-4463-8783-5b21114dc9f6"},{"cell_type":"code","source":"%%file std::move.cpp\n\n#include <iostream>\n#include <vector>\n#include <string>\n\nvoid show( const std::vector< std::string >& vector, const std::string& name ) {\n std::cout << \"Length of vector '\" << name << \"': \" << vector.size()\n << \", contents:\" << std::endl;\n for( const auto& str: vector ) {\n std::cout << str << std::endl;\n }\n std::cout << std::endl;\n}\n\nint main() {\n\n std::vector< std::string > src{ \"Move\", \"Semantics\", \"in\", \"the\", \"STL\" };\n show( src, \"src\" );\n\n std::vector< std::string > dst{ std::move(src) };\n show( dst, \"dst\" );\n show( src, \"src\" );\n\n}","metadata":{"trusted":true},"execution_count":20,"outputs":[{"name":"stdout","text":"Overwriting std::move.cpp\n","output_type":"stream"}],"id":"8e2a7e79-bcd5-4207-a36e-72bcae38b187"},{"cell_type":"code","source":"!g++ std::move.cpp","metadata":{"trusted":true},"execution_count":21,"outputs":[],"id":"fb060696-86ce-440e-8193-5cf540ee6a32"},{"cell_type":"code","source":"!./a.out","metadata":{"trusted":true},"execution_count":22,"outputs":[{"name":"stdout","text":"Length of vector 'src': 5, contents:\nMove\nSemantics\nin\nthe\nSTL\n\nLength of vector 'dst': 5, contents:\nMove\nSemantics\nin\nthe\nSTL\n\nLength of vector 'src': 0, contents:\n\n","output_type":"stream"}],"id":"4712b2fd-4f11-4328-889c-bce3ed9c727e"},{"cell_type":"markdown","source":"Additionally the also support **`move iterators`**. See this nice example on [Fluent C++](https://www.fluentcpp.com/2017/04/25/move-iterators).","metadata":{},"id":"b8b8c25b-ac7c-4263-9dcc-5cc1ea0f5408"},{"cell_type":"markdown","source":"# Rule of Six / Five / Zero","metadata":{},"id":"aa620485-bd12-4727-9c7f-d07f66d0e62a"},{"cell_type":"markdown","source":"The advantage of *move semantics* is that it helps to avoid unnecessary copy operations in order to increase performance. However, it also means that a simple class now could implement the following *six different construction/destruction/assigment methods*:","metadata":{},"id":"6549163b-b1f8-4d7b-9126-0674f01d0022"},{"cell_type":"code","source":"class simple {\n \n // (1) Default Constructor\n simple() {};\n \n // (2) Destructor\n ~simple(){};\n \n // (3) Copy Constructor\n simple( const simple& other ) {};\n \n // (4) Copy Assigment\n simple& operator= ( const simple& other ) {\n simple* aux = new( simple );\n // copy stuff from other to aux\n return *aux;\n };\n \n // (5) Move Constructor\n simple( const simple&& other ) {};\n \n // (6) Move Assignment\n simple& operator= ( const simple&& other ) {\n simple* aux = new( simple );\n // move stuff from other to aux\n // and potentially set other to 'zero'\n return *aux;\n };\n}","metadata":{"trusted":true},"execution_count":14,"outputs":[],"id":"381417d5-fe96-4249-a509-29fcfdd5615b"},{"cell_type":"markdown","source":"The question is, what happens, if we implement none or only some of these methods? For example we know that a default constructor will always be generated automatically by the compiler, if we do not interdict this by deleting it.\n\nThe C++ Core Guidelines have something to say on this topic:\n\n[C.20: If you can avoid defining default operations, do](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-zero)\n> In this case the compiler will auto-generate them for you. This is commonly known as **\"the rule of zero\"**.\n\n[C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-five)\n> The semantics of copy, move, and destruction are closely related, so if one needs to be declared, the odds are that others need consideration too. This is commonly known as **\"the rule of five\"**.\n\n**The Rule of Six**<br>\nextends C.21 to include also the default constructor. The latter is special and, thus, not always include.\n\n**But why?**<br>\nBecause the six special member functions are closely related and the C++ standard is a little bit unintuitive w.r.t. auto-generation when you start selectively defining only some of them. The following table is attributed to *Howard Hinnant (2014)*:<br><br>\n<img src=\"https://www.heise.de/imgs/18/3/6/8/1/8/4/5/DefaultDelete-6dc0661da2aa1431.png\">\n<br><br>\nNote that **user-defined** does not only mean that you implement the constructor/assignment operator, but also includes deleting it (`=delete`) and even explicitely requesting the default variant (`=default`). For full details see \n[Programmiersprache C++: Rule of Zero, or Six](https://www.heise.de/blog/Programmiersprache-C-Rule-of-Zero-or-Six-7463520.html), from which we also borrow the following *cautionary* example:","metadata":{},"id":"4ef8a01b-806b-43b5-9401-5b266143b9e8"},{"cell_type":"code","source":"%%file problematic.cpp\n\n#include <cstddef>\n\nclass BigArray {\n\npublic:\n BigArray( std::size_t len ): len_( len ), data_( new int[ len ] ) {}\n\n ~BigArray() {\n delete[] data_;\n }\n\nprivate:\n size_t len_;\n int* data_;\n \n};\n\nint main() {\n \n BigArray bigArray1( 1000 );\n \n BigArray bigArray2( 1000 );\n \n bigArray2 = bigArray1;\n\n}","metadata":{"trusted":true},"execution_count":2,"outputs":[{"name":"stdout","output_type":"stream","text":"Overwriting problematic.cpp\n"}],"id":"ebe84f93-58ad-47c0-8ae6-9817fab5b867"},{"cell_type":"code","source":"!g++ problematic.cpp","metadata":{"trusted":true},"execution_count":4,"outputs":[],"id":"e45f50e1-2fe8-4a3b-ba00-05edcc21a62b"},{"cell_type":"markdown","source":"Okay, compilation worked. However, when we execute the program we get this:","metadata":{},"id":"79d80da7-ecbe-45d9-9fda-20cfd4af2831"},{"cell_type":"code","source":"!./a.out","metadata":{"trusted":true},"execution_count":6,"outputs":[{"name":"stdout","output_type":"stream","text":"double free or corruption (!prev)\nAborted (core dumped)\n"}],"id":"2510358b-c716-472d-a0f1-ad7bae1546bb"},{"cell_type":"markdown","source":"What is the reason behind this issue?\n\n- Being good C++ programmers, we have implemented a desctructor for `BigArray()` to deallocate the dynamic array `BigArray::data_` when an object of that class is destroyed.\n- Examining the table above, we see that this implies we get the default versions of the other five special member functions. Should be okay, shouldn't it?\n- Well, no:\n - Examine line 26 closer. There we have an assignment. The right-hand side is the named object `bigArray1`, so it is an l-value.\n - Thus, the copy assignment operator will be used here.\n - Its default version will simply generate a copy of all data members. Hence we get a *flat copy* of `bigArray1::data_` and `bigArray1::data_` and `bigArray2::data_` will point to the same memory address.\n - The destructor, however, assumes *ownership* of `data_`. So when both objects go out-of-scope at the end of the program (in line 28), the d'tor of `bigArray1` and that of `bigArray2` both attempt to free the same memory address.\n- So in the end the reason for the problem is that we neglected the rule of zero/five/six.","metadata":{},"id":"78455c0f-b267-436c-a1da-72951b83651d"},{"cell_type":"markdown","source":"When we implement the special member functions we should, of course, avoid confusion be adhering to the following\n\n[C.22: Make default operations consistent](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-matched)\n\nSo, if we implement the copy assignment for `BigArray` to make it a deep copy, the same should hold for the copy constructor [and naturally the move assignment/constructor] like we did in the part on *move semantics*.","metadata":{},"id":"5d9d26b5-05ef-4932-95fb-d5a14b78b501"},{"cell_type":"code","source":"","metadata":{"trusted":true},"execution_count":null,"outputs":[],"id":"865bd92a-b7ce-47b9-9058-edcb272db45e"},{"cell_type":"code","source":"","metadata":{"trusted":true},"execution_count":null,"outputs":[],"id":"719ec3e2-f84b-4109-ab81-ad3b1b8ce71f"}]} \ No newline at end of file +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4c244310-2195-4b50-a923-df0b9ba15f7d", + "metadata": {}, + "source": [ + "# Move Semantics" + ] + }, + { + "cell_type": "markdown", + "id": "92b6cf27-b829-4efd-9395-e38f3a330bb4", + "metadata": {}, + "source": [ + "In order to understand what this is about let us take a look at an example from Rainer Grimm's blog [\"Modernes C++ in der Praxis\"](https://www.linux-magazin.de/ausgaben/2012/12/c-11/):" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4ffa9b8d-02be-4919-882c-18556a6ded7d", + "metadata": {}, + "outputs": [], + "source": [ + "#include <algorithm>\n", + "#include <iostream>\n", + "#include <vector>\n", + "\n", + "class BigArrayCopy {\n", + "\n", + "public:\n", + "\n", + " BigArrayCopy( size_t len ) : len_( len ), data_( new int[ len ] ) {}\n", + "\n", + " BigArrayCopy( const BigArrayCopy& other ) : len_( other.len_ ),\n", + " data_( new int[ other.len_ ] ) {\n", + " std::cout << \"copy construction of \" << other.len_ << \" elements\" << std::endl;\n", + " std::copy( other.data_, other.data_ + len_, data_ );\n", + " }\n", + "\n", + " BigArrayCopy& operator=( const BigArrayCopy& other ) {\n", + "\n", + " std::cout << \"copy assignment of \" << other.len_ << \" elements\" << std::endl;\n", + "\n", + " if( this != &other ) {\n", + "\n", + " delete[] data_;\n", + " len_ = other.len_;\n", + " data_ = new int[ len_ ];\n", + "\n", + " std::copy( other.data_, other.data_ + len_, data_ );\n", + "\n", + " }\n", + "\n", + " return *this;\n", + " }\n", + "\n", + " ~BigArrayCopy() {\n", + " if( data_ != nullptr ) {\n", + " delete[] data_;\n", + " }\n", + " }\n", + " \n", + "private:\n", + "\n", + " size_t len_;\n", + " int* data_;\n", + "\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "dd1e7485-7b8e-4728-a217-df899e07d6d8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "copy construction of 11111111 elements\n", + "copy construction of 11111111 elements\n", + "copy assignment of 22222222 elements\n", + "copy construction of 33333333 elements\n", + "copy construction of 11111111 elements\n" + ] + } + ], + "source": [ + "int main() {\n", + " \n", + " std::vector< BigArrayCopy > myVec;\n", + "\n", + " BigArrayCopy bArray( 11111111 );\n", + " BigArrayCopy bArray2( bArray );\n", + "\n", + " myVec.push_back( bArray );\n", + " bArray = BigArrayCopy( 22222222 );\n", + " myVec.push_back( BigArrayCopy( 33333333 ) );\n", + "\n", + "}\n", + "\n", + "main();" + ] + }, + { + "cell_type": "markdown", + "id": "333e1d6f-aab5-4c70-859d-db4dafb02981", + "metadata": {}, + "source": [ + "Let us analyse the output. Where do the five copies come from?\n", + "<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>\n", + "_____" + ] + }, + { + "cell_type": "markdown", + "id": "6893f7df-99e0-44e9-a79f-8c6e8acba110", + "metadata": {}, + "source": [ + "We can change that by adding a **move constructor** and a **move assignment operator** to our class:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e3784252-4e44-4260-a5a1-d2b92f92e264", + "metadata": {}, + "outputs": [], + "source": [ + "#include <algorithm>\n", + "#include <iostream>\n", + "#include <vector>\n", + "\n", + "using std::cout;\n", + "using std::endl;\n", + "using std::vector;\n", + "\n", + "class BigArray {\n", + "\n", + "public:\n", + "\n", + " BigArray( size_t len ) : len_( len ), data_( new int[ len ] ) {}\n", + "\n", + " BigArray( const BigArray& other ) : len_( other.len_ ),\n", + " data_( new int[ other.len_ ] ) {\n", + " cout << \"copy construction of \" << other.len_ << \" elements\" << endl;\n", + " std::copy( other.data_, other.data_ + len_, data_ );\n", + " }\n", + "\n", + " BigArray& operator=( const BigArray& other ) {\n", + "\n", + " cout << \"copy assignment of \" << other.len_ << \" elements\" << endl;\n", + "\n", + " if( this != &other ) {\n", + "\n", + " delete[] data_;\n", + " len_ = other.len_;\n", + " data_ = new int[ len_ ];\n", + "\n", + " std::copy( other.data_, other.data_ + len_, data_ );\n", + "\n", + " }\n", + "\n", + " return *this;\n", + " }\n", + "\n", + " BigArray( BigArray&& other ) : len_( other.len_ ),\n", + " data_( other.data_ ) {\n", + " cout << \"move construction of \" << other.len_ << \" elements\" << endl;\n", + " other.len_ = 0;\n", + " other.data_ = nullptr;\n", + " }\n", + "\n", + " BigArray& operator=( BigArray&& other ) {\n", + "\n", + " cout << \"move assignment of \" << other.len_ << \" elements\" << endl;\n", + "\n", + " if( this != &other ) {\n", + "\n", + " delete[] data_;\n", + "\n", + " len_ = other.len_;\n", + " data_ = other.data_;\n", + "\n", + " other.len_ = 0;\n", + " other.data_ = nullptr;\n", + " }\n", + "\n", + " return *this;\n", + " }\n", + "\n", + " ~BigArray() {\n", + " if( data_ != nullptr ) {\n", + " delete[] data_;\n", + " }\n", + " }\n", + "private:\n", + "\n", + " size_t len_;\n", + " int* data_;\n", + "\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ebbe2374-3d00-4340-98a0-9352ff8ca2cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "copy construction of 11111111 elements\n", + "copy construction of 11111111 elements\n", + "move assignment of 22222222 elements\n", + "move construction of 33333333 elements\n" + ] + } + ], + "source": [ + "int main() {\n", + " \n", + " std::vector< BigArray > myVec;\n", + " myVec.reserve(2); // get's rid of the final copy operation, when myVec was reallocated\n", + "\n", + " BigArray bArray( 11111111 );\n", + " BigArray bArray2( bArray );\n", + "\n", + " myVec.push_back( bArray );\n", + " bArray = BigArray( 22222222 );\n", + " myVec.push_back( BigArray( 33333333 ) );\n", + "\n", + "}\n", + "\n", + "main();" + ] + }, + { + "cell_type": "markdown", + "id": "2fead1ad-cee3-4a0d-ba70-b14066a349b9", + "metadata": {}, + "source": [ + "As we can see the assignment and copying of the two temporaries in lines # 10 and 11 now uses our **move semantics**.\n", + "\n", + "But what is does the **\"&&\"** in the interface of the two methods represent? It is an **r-value reference**.\n" + ] + }, + { + "cell_type": "markdown", + "id": "a2f2682a-1823-46c4-a444-a23fc192ef17", + "metadata": {}, + "source": [ + "### R-Value References\n", + "\n", + "First of all, what are \"r-values\"? Complicated question, short (simplified) answer: stuff that can appear on the right-hand side of an assignment, hence the name (originally). Examples of r-values are:\n", + "\n", + "- temporary objects\n", + "- unnamed objects\n", + "- objects whose address is undeterminable\n", + "\n", + "such as\n", + "\n", + "```\n", + "int fourtyTwo = 42;\n", + "std::string a = std::string( \"rhs is an rvalue\");\n", + "std::string b = std::string( \"r\" ) + std::string( \"-value\" );\n", + "std::string c = a + b;\n", + "std::string d = std::move(b);\n", + "```\n", + "\n", + "The last line is especially interesting. [std::move](https://en.cppreference.com/w/cpp/utility/move) effectively returns an r-value reference for its argument:\n", + "\n", + "\n", + "> std::move is used to indicate that an object t may be \"moved from\", i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "7c569151-43ef-44a8-babc-6928ca17b626", + "metadata": {}, + "outputs": [], + "source": [ + "int value = 1;\n", + "int& lRef1 = value;\n", + "// int&& rRef1 = value;\n", + "int&& rRef2 = 1;\n", + "const int& lRef2 = 1;" + ] + }, + { + "cell_type": "markdown", + "id": "1ad6ad50-160f-49d9-b486-2ac84c1cbdbb", + "metadata": {}, + "source": [ + "An r-value can bind to an r-value reference, but also to a constant l-value reference. That's why `BigArrayCopy` worked after all. However, binding to an r-value reference, if possible has **higher precedence**. That's what we need in `BigArray` for the move methods." + ] + }, + { + "cell_type": "markdown", + "id": "a0fd8007-04cc-43a5-8c29-07de525a132b", + "metadata": {}, + "source": [ + "#### STL Containers" + ] + }, + { + "cell_type": "markdown", + "id": "87780808-08d4-4463-8783-5b21114dc9f6", + "metadata": {}, + "source": [ + "The containers of the STL support move semantics:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8e2a7e79-bcd5-4207-a36e-72bcae38b187", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Writing std::move.cpp\n" + ] + } + ], + "source": [ + "%%file std::move.cpp\n", + "\n", + "#include <iostream>\n", + "#include <vector>\n", + "#include <string>\n", + "\n", + "void show( const std::vector< std::string >& vector, const std::string& name ) {\n", + " std::cout << \"Length of vector '\" << name << \"': \" << vector.size()\n", + " << \", contents:\" << std::endl;\n", + " for( const auto& str: vector ) {\n", + " std::cout << str << std::endl;\n", + " }\n", + " std::cout << std::endl;\n", + "}\n", + "\n", + "int main() {\n", + "\n", + " std::vector< std::string > src{ \"Move\", \"Semantics\", \"in\", \"the\", \"STL\" };\n", + " show( src, \"src\" );\n", + "\n", + " std::vector< std::string > dst{ std::move(src) };\n", + " show( dst, \"dst\" );\n", + " show( src, \"src\" );\n", + "\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "fb060696-86ce-440e-8193-5cf540ee6a32", + "metadata": {}, + "outputs": [], + "source": [ + "!g++ std::move.cpp" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "4712b2fd-4f11-4328-889c-bce3ed9c727e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Length of vector 'src': 5, contents:\n", + "Move\n", + "Semantics\n", + "in\n", + "the\n", + "STL\n", + "\n", + "Length of vector 'dst': 5, contents:\n", + "Move\n", + "Semantics\n", + "in\n", + "the\n", + "STL\n", + "\n", + "Length of vector 'src': 0, contents:\n", + "\n" + ] + } + ], + "source": [ + "!./a.out" + ] + }, + { + "cell_type": "markdown", + "id": "b8b8c25b-ac7c-4263-9dcc-5cc1ea0f5408", + "metadata": {}, + "source": [ + "Additionally the also support **`move iterators`**. See this nice example on [Fluent C++](https://www.fluentcpp.com/2017/04/25/move-iterators)." + ] + }, + { + "cell_type": "markdown", + "id": "aa620485-bd12-4727-9c7f-d07f66d0e62a", + "metadata": {}, + "source": [ + "# Rule of Six / Five / Zero" + ] + }, + { + "cell_type": "markdown", + "id": "6549163b-b1f8-4d7b-9126-0674f01d0022", + "metadata": {}, + "source": [ + "The advantage of *move semantics* is that it helps to avoid unnecessary copy operations in order to increase performance. However, it also means that a simple class now could implement the following *six different construction/destruction/assigment methods*:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "381417d5-fe96-4249-a509-29fcfdd5615b", + "metadata": {}, + "outputs": [], + "source": [ + "class simple {\n", + " \n", + " // (1) Default Constructor\n", + " simple() {};\n", + " \n", + " // (2) Destructor\n", + " ~simple(){};\n", + " \n", + " // (3) Copy Constructor\n", + " simple( const simple& other ) {};\n", + " \n", + " // (4) Copy Assigment\n", + " simple& operator= ( const simple& other ) {\n", + " simple* aux = new( simple );\n", + " // copy stuff from other to aux\n", + " return *aux;\n", + " };\n", + " \n", + " // (5) Move Constructor\n", + " simple( const simple&& other ) {};\n", + " \n", + " // (6) Move Assignment\n", + " simple& operator= ( const simple&& other ) {\n", + " simple* aux = new( simple );\n", + " // move stuff from other to aux\n", + " // and potentially set other to 'zero'\n", + " return *aux;\n", + " };\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "4ef8a01b-806b-43b5-9401-5b266143b9e8", + "metadata": {}, + "source": [ + "The question is, what happens, if we implement none or only some of these methods? For example we know that a default constructor will always be generated automatically by the compiler, if we do not interdict this by deleting it.\n", + "\n", + "The C++ Core Guidelines have something to say on this topic:\n", + "\n", + "[C.20: If you can avoid defining default operations, do](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-zero)\n", + "> In this case the compiler will auto-generate them for you. This is commonly known as **\"the rule of zero\"**.\n", + "\n", + "[C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-five)\n", + "> The semantics of copy, move, and destruction are closely related, so if one needs to be declared, the odds are that others need consideration too. This is commonly known as **\"the rule of five\"**.\n", + "\n", + "**The Rule of Six**<br>\n", + "extends C.21 to include also the default constructor. The latter is special and, thus, not always include.\n", + "\n", + "**But why?**<br>\n", + "Because the six special member functions are closely related and the C++ standard is a little bit unintuitive w.r.t. auto-generation when you start selectively defining only some of them. The following table is attributed to *Howard Hinnant (2014)*:<br><br>\n", + "<img src=\"https://www.heise.de/imgs/18/3/6/8/1/8/4/5/DefaultDelete-6dc0661da2aa1431.png\">\n", + "<br><br>\n", + "Note that **user-defined** does not only mean that you implement the constructor/assignment operator, but also includes deleting it (`=delete`) and even explicitely requesting the default variant (`=default`). For full details see \n", + "[Programmiersprache C++: Rule of Zero, or Six](https://www.heise.de/blog/Programmiersprache-C-Rule-of-Zero-or-Six-7463520.html), from which we also borrow the following *cautionary* example:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "ebe84f93-58ad-47c0-8ae6-9817fab5b867", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting problematic.cpp\n" + ] + } + ], + "source": [ + "%%file problematic.cpp\n", + "\n", + "#include <cstddef>\n", + "#include <utility>\n", + "\n", + "class BigArray {\n", + "\n", + "public:\n", + " BigArray( std::size_t len ): len_( len ), data_( new int[ len ] ) {}\n", + "\n", + " ~BigArray() {\n", + " delete[] data_;\n", + " }\n", + "\n", + "private:\n", + " size_t len_;\n", + " int* data_;\n", + " \n", + "};\n", + "\n", + "int main() {\n", + " \n", + " BigArray bigArray1( 1000 );\n", + " \n", + " BigArray bigArray2( 1000 );\n", + " \n", + " bigArray2 = std::move( bigArray1 );\n", + "\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "e45f50e1-2fe8-4a3b-ba00-05edcc21a62b", + "metadata": {}, + "outputs": [], + "source": [ + "!g++ problematic.cpp" + ] + }, + { + "cell_type": "markdown", + "id": "79d80da7-ecbe-45d9-9fda-20cfd4af2831", + "metadata": {}, + "source": [ + "Okay, compilation worked. However, when we execute the program we get this:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "2510358b-c716-472d-a0f1-ad7bae1546bb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "double free or corruption (!prev)\n", + "Aborted (core dumped)\n" + ] + } + ], + "source": [ + "!./a.out" + ] + }, + { + "cell_type": "markdown", + "id": "78455c0f-b267-436c-a1da-72951b83651d", + "metadata": {}, + "source": [ + "What is the reason behind this issue?\n", + "\n", + "- Being good C++ programmers, we have implemented a desctructor for `BigArray()` to deallocate the dynamic array `BigArray::data_` when an object of that class is destroyed.\n", + "- Examining the table above, we see that this implies we get the default versions of the other five special member functions. Should be okay, shouldn't it?\n", + "- Well, no:\n", + " - Examine line 26 closer. There we have an assignment. The right-hand side is the named object `bigArray1`, so it is an l-value.\n", + " - Thus, the copy assignment operator will be used here.\n", + " - Its default version will simply generate a copy of all data members. Hence we get a *flat copy* of `bigArray1::data_` and `bigArray1::data_` and `bigArray2::data_` will point to the same memory address.\n", + " - The destructor, however, assumes *ownership* of `data_`. So when both objects go out-of-scope at the end of the program (in line 28), the d'tor of `bigArray1` and that of `bigArray2` both attempt to free the same memory address.\n", + "- So in the end the reason for the problem is that we neglected the rule of zero/five/six." + ] + }, + { + "cell_type": "markdown", + "id": "5d9d26b5-05ef-4932-95fb-d5a14b78b501", + "metadata": {}, + "source": [ + "When we implement the special member functions we should, of course, avoid confusion be adhering to the following\n", + "\n", + "[C.22: Make default operations consistent](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-matched)\n", + "\n", + "So, if we implement the copy assignment for `BigArray` to make it a deep copy, the same should hold for the copy constructor [and naturally the move assignment/constructor] like we did in the part on *move semantics*." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "865bd92a-b7ce-47b9-9058-edcb272db45e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "C++14", + "language": "C++14", + "name": "xcpp14" + }, + "language_info": { + "codemirror_mode": "text/x-c++src", + "file_extension": ".cpp", + "mimetype": "text/x-c++src", + "name": "c++", + "version": "14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} -- GitLab