Skip to content
Snippets Groups Projects
Commit 93fa73ba authored by Marcus Mohr's avatar Marcus Mohr
Browse files

Start work on two new notebooks

- Move semantics and rule of six
- Emulating Multiple Dispatch in C++
parent e9cea1a4
Branches
No related tags found
No related merge requests found
%% Cell type:markdown id:4c244310-2195-4b50-a923-df0b9ba15f7d tags:
# Move Semantics
%% Cell type:markdown id:92b6cf27-b829-4efd-9395-e38f3a330bb4 tags:
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":
%% Cell type:code id:4ffa9b8d-02be-4919-882c-18556a6ded7d tags:
``` C++17
#include <algorithm>
#include <iostream>
#include <vector>
class BigArrayCopy {
public:
BigArrayCopy( size_t len ) : len_( len ), data_( new int[ len ] ) {}
BigArrayCopy( const BigArrayCopy& other ) : len_( other.len_ ),
data_( new int[ other.len_ ] ) {
std::cout << "copy construction of " << other.len_ << " elements" << std::endl;
std::copy( other.data_, other.data_ + len_, data_ );
}
BigArrayCopy& operator=( const BigArrayCopy& other ) {
std::cout << "copy assignment of " << other.len_ << " elements" << std::endl;
if( this != &other ) {
delete[] data_;
len_ = other.len_;
data_ = new int[ len_ ];
std::copy( other.data_, other.data_ + len_, data_ );
}
return *this;
}
~BigArrayCopy() {
if( data_ != nullptr ) {
delete[] data_;
}
}
private:
size_t len_;
int* data_;
};
```
%% Cell type:code id:dd1e7485-7b8e-4728-a217-df899e07d6d8 tags:
``` C++17
int main() {
std::vector< BigArrayCopy > myVec;
BigArrayCopy bArray( 11111111 );
BigArrayCopy bArray2( bArray );
myVec.push_back( bArray );
bArray = BigArrayCopy( 22222222 );
myVec.push_back( BigArrayCopy( 33333333 ) );
}
main();
```
%% Output
copy construction of 11111111 elements
copy construction of 11111111 elements
copy assignment of 22222222 elements
copy construction of 33333333 elements
copy construction of 11111111 elements
%% Cell type:markdown id:333e1d6f-aab5-4c70-859d-db4dafb02981 tags:
Let us analyse the output. Where do the five copies come frome?
%% Cell type:markdown id:6893f7df-99e0-44e9-a79f-8c6e8acba110 tags:
We can change that by adding a **move constructor** and a **move assignment operator** to our class:
%% Cell type:code id:e3784252-4e44-4260-a5a1-d2b92f92e264 tags:
``` C++17
#include <algorithm>
#include <iostream>
#include <vector>
using std::cout;
using std::endl;
using std::vector;
class BigArray {
public:
BigArray( size_t len ) : len_( len ), data_( new int[ len ] ) {}
BigArray( const BigArray& other ) : len_( other.len_ ),
data_( new int[ other.len_ ] ) {
cout << "copy construction of " << other.len_ << " elements" << endl;
std::copy( other.data_, other.data_ + len_, data_ );
}
BigArray& operator=( const BigArray& other ) {
cout << "copy assignment of " << other.len_ << " elements" << endl;
if( this != &other ) {
delete[] data_;
len_ = other.len_;
data_ = new int[ len_ ];
std::copy( other.data_, other.data_ + len_, data_ );
}
return *this;
}
BigArray( BigArray&& other ) : len_( other.len_ ),
data_( other.data_ ) {
cout << "move construction of " << other.len_ << " elements" << endl;
other.len_ = 0;
other.data_ = nullptr;
}
BigArray& operator=( BigArray&& other ) {
cout << "move assignment of " << other.len_ << " elements" << endl;
if( this != &other ) {
delete[] data_;
len_ = other.len_;
data_ = other.data_;
other.len_ = 0;
other.data_ = nullptr;
}
return *this;
}
~BigArray() {
if( data_ != nullptr ) {
delete[] data_;
}
}
private:
size_t len_;
int* data_;
};
```
%% Cell type:code id:ebbe2374-3d00-4340-98a0-9352ff8ca2cb tags:
``` C++17
int main() {
std::vector< BigArray > myVec;
myVec.reserve(2); // get's rid of the final copy operation, when myVec was reallocated
BigArray bArray( 11111111 );
BigArray bArray2( bArray );
myVec.push_back( bArray );
bArray = BigArray( 22222222 );
myVec.push_back( BigArray( 33333333 ) );
}
main();
```
%% Output
copy construction of 11111111 elements
copy construction of 11111111 elements
move assignment of 22222222 elements
move construction of 33333333 elements
%% Cell type:markdown id:2fead1ad-cee3-4a0d-ba70-b14066a349b9 tags:
As we can see the assignment and copying of the two temporaries in lines # 10 and 11 now uses our **move semantics**.
But what is does the **"&&"** in the interface of the two methods represent? It is an **r-value reference**.
%% Cell type:markdown id:a2f2682a-1823-46c4-a444-a23fc192ef17 tags:
### R-Value References
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:
- temporary objects
- unnamed objects
- objects whose address is undeterminable
such as
```
int fourtyTwo = 42;
std::string a = std::string( "rhs is an rvalue");
std::string b = std::string( "r" ) + std::string( "-value" );
std::string c = a + b;
std::string d = std::move(b);
```
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:
> 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 id:7c569151-43ef-44a8-babc-6928ca17b626 tags:
``` C++17
int value = 1;
int& lRef1 = value;
// int&& rRef1 = value;
int&& rRef2 = 1;
const int& lRef2 = 1;
```
%% Cell type:markdown id:1ad6ad50-160f-49d9-b486-2ac84c1cbdbb tags:
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:aa620485-bd12-4727-9c7f-d07f66d0e62a tags:
# Rule of Six / Five / Zero
%% Cell type:markdown id:6549163b-b1f8-4d7b-9126-0674f01d0022 tags:
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 id:381417d5-fe96-4249-a509-29fcfdd5615b tags:
``` C++17
class simple {
// Default Constructor
simple() {};
// Destructor
~simple(){};
// Copy Constructor
simple( const simple& other ) {};
// Copy Assigment
simple& operator= ( const simple& other ) {
simple* aux = new( simple );
// copy stuff from other to aux
return *aux;
};
// Move Constructor
simple( const simple&& other ) {};
// Move Assignment
simple& operator= ( const simple&& other ) {
simple* aux = new( simple );
// move stuff from other to aux
// and potentially set other to 'zero'
return *aux;
};
}
```
%% Cell type:markdown id:4ef8a01b-806b-43b5-9401-5b266143b9e8 tags:
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.
%% Cell type:code id:ebe84f93-58ad-47c0-8ae6-9817fab5b867 tags:
``` C++17
https://www.heise.de/blog/Programmiersprache-C-Rule-of-Zero-or-Six-7463520.html
```
%% Cell type:code id:e45f50e1-2fe8-4a3b-ba00-05edcc21a62b tags:
``` C++17
```
%% Cell type:markdown id:559dbab5-4930-41cf-ab02-99ed7e282149 tags:
# Multiple Dispatch
In his [Geocomputing presentation](https://www.geophysik.uni-muenchen.de/en/seminars/seminars/geocomputing-29/t-b-a-6) Roman Freissler mentioned **multiple dispatch** as one of the features of the programming language **Julia**. So what is it?
### Wikipedia:
> Multiple dispatch or multimethods is a feature of some programming languages in which a function or method can be dynamically dispatched based on the run-time (dynamic) type or, in the more general case, some other attribute of more than one of its arguments.[1] This is a generalization of single-dispatch polymorphism where a function or method call is dynamically dispatched based on the derived type of the object on which the method has been called. Multiple dispatch routes the dynamic dispatch to the implementing function or method using the combined characteristics of one or more arguments.
```
abstract type SpaceObject end
struct Asteroid <: SpaceObject
size::Int
end
struct Spaceship <: SpaceObject
size::Int
end
collide_with(::Asteroid, ::Spaceship) = "a/s"
collide_with(::Spaceship, ::Asteroid) = "s/a"
collide_with(::Spaceship, ::Spaceship) = "s/s"
collide_with(::Asteroid, ::Asteroid) = "a/a"
collide(x::SpaceObject, y::SpaceObject) = (x.size > 100 && y.size > 100) ? "Big boom!" : collide_with(x, y)
julia> collide(Asteroid(101), Spaceship(300))
"Big boom!"
julia> collide(Asteroid(10), Spaceship(10))
"a/s"
julia> collide(Spaceship(101), Spaceship(10))
"s/s"
```
%% Cell type:markdown id:58bd10b2-159d-473b-962d-3b09fd7437cd tags:
## C++
C++ does support *dynamic dispatch*, but only in the form of *single dispatch*. Consider the following example:
%% Cell type:code id:c5d388d8-79d7-49d7-a37c-4805521920a1 tags:
``` C++17
#include <iostream>
struct Animal {
virtual void makeSound() const = 0;
};
struct Cat : public Animal {
void makeSound() const override {
std::cout << "Meow" << std::endl;
}
};
struct Dog : public Animal {
void makeSound() const override {
std::cout << "Wuff" << std::endl;
}
};
```
%% Cell type:code id:bf13fc00-47e6-449c-af3d-4f45f84559ac tags:
``` C++17
int main() {
Cat boss;
Dog partner;
Animal* animal1{ &boss };
Animal* animal2{ &partner };
animal1->makeSound();
animal2->makeSound();
}
main();
```
%% Output
Meow
Wuff
%% Cell type:markdown id:18e383f7-a368-42f9-b398-c4a8d13fc175 tags:
The decision which member function `makeSound` is to be called is taken at runtime depending on the type of child object of `Animal` the pointer points to. Conceptually this happens by examining the first *hidden argument* of `makeSound( this, ... )`.
While C++ does **not support multiple dispatch**, we can emulate it in various ways (examples modified from Wikipedia).
#### Multiple Dispatch via Dynamic Casting
%% Cell type:code id:abec11bc-6cc1-4100-a2df-0e308ffc71ed tags:
``` C++17
%%file md_with_casting.cpp
#include <iostream>
struct SpaceObject {
SpaceObject( int in ) : size(in) {};
virtual void collideWith( SpaceObject& other ) = 0;
int size;
};
// Need to split class declaration and method implementation
// because we can only cast complete types, thus, a forward
// declaration is not sufficient.
struct Asteroid : SpaceObject {
Asteroid( int in ) : SpaceObject( in ) {};
void collideWith( SpaceObject& other );
};
struct Spaceship : SpaceObject {
Spaceship( int in ) : SpaceObject( in ) {};
void collideWith( SpaceObject& other );
};
void Asteroid::collideWith( SpaceObject& other ) {
// dynamic_cast to a pointer type returns NULL if the cast fails
// (dynamic_cast to a reference type would throw an exception on failure)
if( auto asteroid = dynamic_cast< Asteroid* >( &other ) ) {
// handle Asteroid-Asteroid collision
std::cout << "Asteroid-Asteroid collision!" << std::endl;
} else if( auto spaceship = dynamic_cast< Spaceship* >( &other ) ) {
// handle Asteroid-Spaceship collision
std::cout << "Asteroid-Spaceship collision!" << std::endl;
} else {
// default collision handling here
std::cout << "Asteroid-UFO collision!" << std::endl;
}
}
void Spaceship::collideWith( SpaceObject& other ) {
if( auto asteroid = dynamic_cast< Asteroid* >( &other ) ) {
// handle Spaceship-Asteroid collision
std::cout << "Spaceship-Asteroid collision!" << std::endl;
} else if( auto spaceship = dynamic_cast< Spaceship* >( &other ) ) {
// handle Spaceship-Spaceship collision
std::cout << "Spaceship-Spaceship collision!" << std::endl;
} else {
// default collision handling here
std::cout << "Spaceship-UFO collision!" << std::endl;
}
}
void collide( SpaceObject& x, SpaceObject& y ) {
if( x.size > 100 && y.size > 100 ) {
std::cout << "Big boom!" << std::endl;
}
else {
x.collideWith( y );
}
}
int main() {
Asteroid asteroid{101};
Spaceship spaceship{300};
collide( asteroid, spaceship );
asteroid.size = 10;
spaceship.size = 10;
collide( asteroid, spaceship );
collide( asteroid, asteroid );
collide( spaceship, asteroid );
collide( spaceship, spaceship );
}
```
%% Output
Writing md_with_casting.cpp
%% Cell type:code id:547cb38a-82e2-4f62-9b6e-23d5462087e6 tags:
``` C++17
!g++ md_with_casting.cpp
```
%% Cell type:code id:b60b5e86-e09b-4e48-938d-956626d6b5c9 tags:
``` C++17
!./a.out
```
%% Output
Big boom!
Asteroid-Spaceship collision!
Asteroid-Asteroid collision!
Spaceship-Asteroid collision!
Spaceship-Spaceship collision!
%% Cell type:markdown id:bc7899b8-32f8-4c31-bc5b-7c7f5f43fe49 tags:
#### Multiple Dispatch via Map
%% Cell type:code id:00f07cb8-9b9f-49f9-aaa4-251edbedf7a6 tags:
``` C++17
%%file md_with_map.cpp
#include <cstdint>
#include <typeinfo>
#include <unordered_map>
#include <iostream>
class Thing {
protected:
Thing( std::uint32_t cid ) : tid( cid ) {}
const std::uint32_t tid; // type id
using CollisionHandler = void(Thing::*)(Thing& other);
using CollisionHandlerMap = std::unordered_map< std::uint64_t, CollisionHandler >;
static void addHandler( std::uint32_t id1, std::uint32_t id2, CollisionHandler handler ) {
collisionCases.insert( CollisionHandlerMap::value_type( key( id1, id2 ), handler ) );
}
static std::uint64_t key( std::uint32_t id1, std::uint32_t id2 ) {
return std::uint64_t(id1) << 32 | id2;
}
static CollisionHandlerMap collisionCases;
public:
void collideWith( Thing& other ) {
auto handler = collisionCases.find( key( tid, other.tid ) );
if ( handler != collisionCases.end() ) {
( this->*(handler->second) )( other ); // pointer-to-method call
// ( this->*handler->second )( other ); // pointer-to-method call
} else {
std::cout << "Collision of unknown type!" << std::endl;
}
}
};
class Asteroid: public Thing {
void asteroid_collision( Thing& other ) {
std::cout << "Asteroid-Asteroid collision!" << std::endl;
}
void spaceship_collision( Thing& other ) {
std::cout << "Asteroid-Spaceship collision!" << std::endl;
}
public:
Asteroid(): Thing( cid ) {}
static void initCases();
static const std::uint32_t cid;
};
class Spaceship: public Thing {
void asteroid_collision( Thing& other ) {
std::cout << "Spaceship-Asteroid collision!" << std::endl;
}
void spaceship_collision( Thing& other ) {
std::cout << "Spaceship-Spaceship collision!" << std::endl;
}
public:
Spaceship(): Thing( cid ) {}
static void initCases();
static const std::uint32_t cid; // class id
};
Thing::CollisionHandlerMap Thing::collisionCases;
const std::uint32_t Asteroid::cid = typeid( Asteroid ).hash_code();
const std::uint32_t Spaceship::cid = typeid( Spaceship ).hash_code();
void Asteroid::initCases() {
addHandler( cid, cid, CollisionHandler(&Asteroid::asteroid_collision ) );
addHandler( cid, Spaceship::cid, CollisionHandler(&Asteroid::spaceship_collision ) );
}
void Spaceship::initCases() {
addHandler( cid, Asteroid::cid, CollisionHandler( &Spaceship::asteroid_collision ) );
addHandler( cid, cid, CollisionHandler( &Spaceship::spaceship_collision ) );
}
int main() {
Asteroid::initCases();
Spaceship::initCases();
Asteroid a1, a2;
Spaceship s1, s2;
a1.collideWith( a2 );
a1.collideWith( s1 );
s1.collideWith( s2 );
s1.collideWith( a1 );
}
```
%% Output
Writing md_with_map.cpp
%% Cell type:code id:add9aaee-5fac-4206-8d00-de314f99631e tags:
``` C++17
!g++ md_with_map.cpp
```
%% Cell type:code id:1092b652-e9df-4b12-9295-82fff8923817 tags:
``` C++17
!./a.out
```
%% Output
Asteroid-Asteroid collision!
Asteroid-Spaceship collision!
Spaceship-Spaceship collision!
Spaceship-Asteroid collision!
%% Cell type:code id:24efc2a0-8297-489d-abd1-d76abb15711c tags:
``` C++17
```
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment