Move-Semantik - Zwei nette Eigenschaften

In diesem Artikel werde ich zwei nette Eigenschaften der Move-Semantik vorstellen, die mir zu selten erwähnt werden. Container der Standard Template Library (STL) können nicht kopierbare Elemente besitzen. Die Copy-Semantik ist der Fallback für die Move-Semantik. Verwirrt? Absicht!

 

Verschieben statt kopieren

Kannst du dich an das Programm packagedTask.cpp aus dem Artikel Aufrufbar asynchrone Wrapper noch erinnern? Sicher nicht. Hier ist es nochmals.

Elemente in den Container verschieben

 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;

}

 

Mich interessiert absolut nicht, dass in dem Programm die Summe der Zahlen von 0 .. 10000 in vier Threads berechnet wird.

Mich interessiert eine ganze andere Eigenschaft von std::packaged_task. std::packaged_task ist nicht kopierbar. Der Grund ist trivial. Der Copy-Konstruktor und Copy-Zuweisungsoperator sind auf delete gesetzt. Schwarz auf weiß lässt sich das auf cppreference.com nachlesen.

Trotzdem ist es möglich, die std::packaged_task Objekte als Elemente eines Containers der STL zu verwenden. Container der STL wollen ihre Elemente besitzen. Daher verschiebe ich mittels std::move die std::packaged_task Objekte in den Zeilen 41 - 44 direkt in den Container std::deque. Auch in den Zeilen 52 und 54 muss ich Move-Semantik einsetzen, da sich die std::packaged_task nicht kopieren lassen.

Aber das ist noch nicht das Ende der Geschichte. Wendet ein Algorithmus der STL unter der Decke keine Copy-Semantik an, lässt sich auch ein Container mit nicht kopierbaren Elementen bearbeiten. Wendet der Container doch Copy-Semantik an, gibt es einen Fehler zur Compilezeit.

Algorithmen auf nur verschiebbaren Elementen

In meinem nächsten Beispiel mache ich es mir sehr leicht. Ich definiere einen einfachen Wrapper MyInt um eine natürliche Zahl. 

 

 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 <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;
    
}

 

Rein deklarativ setze ich die Copy-Semantik auf delete (Zeile 13 und 14) und die Move-Semantik (Zeile 17 und 18) auf default. Damit sorgt der Compiler für die richtige Implementierung. Lediglich den Konstruktor und die Zugriffsfunktion getVal (Zeile 20 - 22) implementiere ich. Obwohl der Typ MyInt per Definition nicht kopierbar ist, verwende ich ihn in den Algorithmen std::for_each (Zeile 36) und std::accumulate (Zeile 40) der STL.

Das Ergebnis birgt keine große Überraschung.

moveAlgorithm

Die Copy-Semantik ist ein Fallback für die Move-Semantik. Was heißt das?

Copy-Semantik als Fallback für die Move-Semantik

Schreibe ich einen Algorithmus, der intern Move-Semantik verwendet, kann dieser Algorithmus auch auf Typen angewandt werden, die nur Copy-Semantik unterstützen. Dazu muss ich meinen Typ MyInt modifizieren.

 

 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 besitzt in diesem Beispiel einen benutzerdefinierten Copy-Konstruktor und Copy-Zuweisungsoperator. Damit erzeugt der Compiler nicht automatisch einen Move-Konstruktor oder Move-Zuweisungsoperator. Dieses Verhalten lässt sich auf der cppreference.com Seite zur Move-Semantik nachlesen oder mit Hilfe der Type-Traits Bibliothek (Zeile 40 und 41) abfragen. Leider unterstützt mein GCC die Funktion der Type-Traits Bibliothek noch nicht, so dass ich den Code auf einem modernen GCC 5.2 online ausgeführt habe. Der entscheidende Punkt ist, dass Instanzen vom Typ MyInt in dem Funktions-Template swapMove (Zeile 7 - 12) verwendet werden können, obwohl diese keine Move-Semantik unterstützen. 

copyFallbackMove

 

Der Grund ist. Ein Rvalue kann an eine

  • konstante Lvalue Referenz
  • nicht konstante Rvalue Referenz

gebunden werden. Im Zweifelsfall besitzt die nicht konstante Rvalue Referenz Vorrang. Ein Copy-Konstruktor oder Copy-Zuweisungsoperatur erwartet sein Argument als konstante Lvalue Referenz, ein Move-Konstruktor oder Move-Zuweisungsoperator als nicht konstante Rvalue Referenz. Was sich ein bisschen verwirrend anhört, besitzt eine sehr schöne Konsequenz.

Die Performanz im Hinterkopf

Du kannst deine Funktionen wie swapMove auf Performanz getrimmt mit Move-Semantik umsetzen. Unterstützt dein Datentyp noch nicht die Move-Semantik, ist das Programm trotzdem syntaktisch richtig. Der Compiler wendet in diesem Fall die klassische Copy-Semantik an. Damit lässt sich sehr komfortabel eine alte Codebasis sukzessive auf modernes C++ migrieren. Das Programm ist in der ersten Iteration korrekt, in der zweiten schneller.

Wie geht's weiter?

Funktions-Templates zu schreiben, die ihre Argumente identisch weiterreichen, das ist erst mit C++11 und std::forward möglich. Perfekt Forwarding war  " ... a heretofore unsolved problem in C++.". (Bjarne Stroustrup). Genau darum geht es im nächsten Artikel. 

 

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Hole dir dein E-Book. Unterstütze meinen Blog.

 

 

Tags: move

Mentoring

Stay Informed about my Mentoring

 

Rezensionen

Tutorial

Besucher

Heute 410

Gestern 1599

Woche 6012

Monat 21954

Insgesamt 3320866

Aktuell sind 40 Gäste und keine Mitglieder online

Kubik-Rubik Joomla! Extensions

Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare