Heute lösen wir ein bisher ungelöstes Problem in klassischem C++: " ... a herefore unsolved problem in C++" (Bjarne Stroustrup). Um es kurz zu machen, es geht um Perfect Forwarding.
Doch was ist Perfect Forwarding.
Reicht ein Funktion-Template seine Argumente identisch weiter, ohne deren Lvalue oder Rvalue Charakteristik zu ändern, wird dies Perfect Forwarding genannt.
Super. Und was sind Lvalues und Rvalues? Da muss ich ein wenig ausholen.
Lvalues und Rvalues
Ich will in dieser Erläuterung nicht auf die Details zu Lvalues und Rvalues eingehen und darüber hinaus auch glvalue, xvalue und prvalue erklären. Das ist zu verwirrend und kontraproduktiv. Falls du neugierig bist, verweise ich gerne auf den Artikel von Anthony Williams: Core C++ - lvalues and rvalues. Ich werde statt dessen eine tragfähige Intuition aufbauen.
Rvalues sind:
- temporäre Objekte
- Objekte ohne Namen
- Objekte, deren Adresse sich nicht bestimmen lässt
Trifft auf Objekte eines dieser Charakteristika zu, liegt ein Rvalue vor. Im Umkehrschluss bedeutet dies, dass Lvalues einen Namen und eine Adresse besitzen. Ein paar Beispiele für Rvalues:
int five= 5;
std::string a= std::string("Rvalue");
std::string b= std::string("R") + std::string("value");
std::string c= a + b;
std::string d= std::move(b);
Rvalues stehen auf der rechten Seite einer Zuweisung. So sind in den Beispielen der Wert 5 und der Konstruktoraufruf std::string("Rvalue") Rvalues, denn weder lässt sich für den Wert 5 die Adresse bestimmen, noch besitzt der Konstruktoraufruf einen Namen. Gleiches gilt für die Addition der Rvalues in dem Ausdruck std::string("R") + std::string("value").
Interessant ist die String-Addition zweier Lvalues in a + b. Dieser Ausdruck wird zum Rvalue, da die Addition zweier Lvalues ein temporäres Objekt erzeugt. Ein besonderer Anwendungsfall ist std::move(b). Die neue C++11-Funktion konvertiert den Lvalue b in eine Rvalue-Referenz.
Rvalues stehen auf der rechten Seite einer Zuweisung, Lvalues können auch auf der linken Seite einer Zuweisung stehen. Dies gilt aber nicht immer.
const int five= 5;
five= 6;
Zwar ist die Variable five ein Lvalue. Da sie konstant ist, kann sie aber nicht auf der linken Seite einer Zuweisung stehen.
Nun aber zu der eigentlichen Herausforderung: Perfect Forwarding. Um das bisher ungelöste Problem intuitiv zu erfassen, werde ich sukzessive eine perfekte Fabrikmethode entwickeln.
Eine perfekte Fabrikfunktion
Zuerst ein kleiner Disclaimer. Der Ausdruck perfekte Fabrikfunktion ist kein formal definierter Ausdruck.
Unter einer perfekten Fabrikfunktion verstehe ich eine vollkommen generische Fabrikfunktion. Das heißt insbesondere, dass die Funktion die folgenden Eigenschaften besitzen soll:
- Ein beliebige Anzahl von Argumenten annehmen
- Sowohl Lvalues als auch Rvalues als Argumente akzeptieren
- Die Funktionsargumente identisch an das zu erzeugende Objekte weiterreichen
Das Ganze geht auch weniger formal. Eine perfekte Fabrikfunktion soll jedes beliebige Objekt erzeugen können. Und los geht es mit der ersten Iteration.
Erste Iteration
Aus Effizienzgründen übergebe ich das Funktionsargument als Referenz. Genau gesprochen, als nicht konstante Lvalue-Referenz. Das Funktions-Template create in der ersten Iteration.
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
|
// perfectForwarding1.cpp
#include <iostream>
template <typename T,typename Arg>
T create(Arg& a){
return T(a);
}
int main(){
std::cout << std::endl;
// Lvalues
int five=5;
int myFive= create<int>(five);
std::cout << "myFive: " << myFive << std::endl;
// Rvalues
int myFive2= create<int>(5);
std::cout << "myFive2: " << myFive2 << std::endl;
std::cout << std::endl;
}
|
Übersetze ich das Programm, moniert der Compiler, dass er den Rvalue (Zeile 21) nicht an eine nicht konstante Lvalue-Referenz binden kann.
Jetzt bieten sich zwei Lösungsmöglichkeiten an.
- Ändere die nicht konstante Lvalue Referenz (Zeile 6) in eine konstante Lvalue Referenz. An eine konstante Lvalue Referenz kann auch eine Rvalue gebunden werden. Das ist aber alles andere als perfekt, denn nun kann das Funktionsargument nicht mehr verändert werden.
- Überlade das Funktions-Template für eine konstante Lvalue-Referenz und eine nicht konstante Lvalue-Referenz. Das ist einfach.
Zweite Iteration
Die Fabrikfunktion create für eine konstante Lvalue-Referenz und eine nicht konstante Lvalue-Referenz überladen.
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
|
// perfectForwarding2.cpp
#include <iostream>
template <typename T,typename Arg>
T create(Arg& a){
return T(a);
}
template <typename T,typename Arg>
T create(const Arg& a){
return T(a);
}
int main(){
std::cout << std::endl;
// Lvalues
int five=5;
int myFive= create<int>(five);
std::cout << "myFive: " << myFive << std::endl;
// Rvalues
int myFive2= create<int>(5);
std::cout << "myFive2: " << myFive2 << std::endl;
std::cout << std::endl;
}
|
Das Programm liefert das erwartete Ergebnis.
Das war einfach. Zu einfach. Die Lösung besitzt zwei konzeptionelle Probleme.
- Um n verschiedene Argumente zu unterstützen, müssen 2^n +1 Variationen der Funktions-Templates create implementiert werden. 2^n +1, da zu einer perfekten Lösung auch das Funktions-Template ohne Argumente gehört.
- Das Funktionsargument ist im Funktionskörper von create zum Lvalue mutiert, denn a besitzt einen Namen. Stört das? Ja! Jetzt muss teuer kopiert werden was billig verschoben werden könnte.
Nun naht aber die Lösung in Form der neuen C++-Funktion std::forward.
Dritte Iteration
Mit std::forward wird die Lösung deutlich eleganter.
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
|
// perfectForwarding3.cpp
#include <iostream>
template <typename T,typename Arg>
T create(Arg&& a){
return T(std::forward<Arg>(a));
}
int main(){
std::cout << std::endl;
// Lvalues
int five=5;
int myFive= create<int>(five);
std::cout << "myFive: " << myFive << std::endl;
// Rvalues
int myFive2= create<int>(5);
std::cout << "myFive2: " << myFive2 << std::endl;
std::cout << std::endl;
}
|
Bevor ich das Rezept aus cppreference.com bescheibe, um Perfect Forwarding zu erhalten, möchte ich kurz noch den Namen Universelle-Referenz vorstellen.
Der Begriff Universelle-Referenz geht zurück auf Scott Meyers.
Die Universelle-Referenz (Arg&& a) in Zeile 7 ist eine besonderes mächtige Referenz, die sowohl Lvalues als auch Rvalues binden kann. Sie entsteht genau dann, wenn eine Variable Arg&&
a für einen abgeleiteten Typ Arg
erklärt wird.
Um nun Perfect Forwarding zu erhalten, benötigt es das Zusammenspiel einer Universellen-Referenz mit std::forward. std::forward<Arg>(a) gibt den zugrundeliegenden Typ von a weiter, da a eine Universelle Referenz ist. Damit bleibt ein Rvalue ein Rvalue.
Nun zum Muster.
template<class T>
void wrapper(T&& a){
func(std::forward<T>(a));
}
Die entscheidenden Bestandteile des Musters habe ich rot hinterlegt. Genau das Muster kommt in dem Funktions-Template create zum Einsatz. Lediglich der Typename ändert sich von T auf Arg.
Ist das Funktions-Template create nun perfekt? Leider nicht. create benötigt genau ein Argument, das es an den Konstruktor des Objektes perfekt weiterleitet (Zeile 7). Nun ist es in der letzten Iteration noch notwendig, das Funktions-Template zum Variadic Template zu erweitern.
Vierte Iteration - Die perfekte Fabrikfunktion
Variadic Templates sind Templates, die beliebig viele Argumente annehmen können. Genau diese Eigenschaft fehlt der perfekten Fabrikfunktion.
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
|
// perfectForwarding4.cpp
#include <iostream>
#include <string>
#include <utility>
template <typename T, typename ... Args>
T create(Args&& ... args){
return T(std::forward<Args>(args)...);
}
struct MyStruct{
MyStruct(int i,double d,std::string s){}
};
int main(){
std::cout << std::endl;
// Lvalues
int five=5;
int myFive= create<int>(five);
std::cout << "myFive: " << myFive << std::endl;
std::string str{"Lvalue"};
std::string str2= create<std::string>(str);
std::cout << "str2: " << str2 << std::endl;
// Rvalues
int myFive2= create<int>(5);
std::cout << "myFive2: " << myFive2 << std::endl;
std::string str3= create<std::string>(std::string("Rvalue"));
std::cout << "str3: " << str3 << std::endl;
std::string str4= create<std::string>(std::move(str3));
std::cout << "str4: " << str4 << std::endl;
// Arbitrary number of arguments
double doub= create<double>();
std::cout << "doub: " << doub << std::endl;
MyStruct myStr= create<MyStruct>(2011,3.14,str4);
std::cout << std::endl;
}
|
Die drei Punkte in den Zeilen 7 - 9 sind ein sogenanntes Parameter Pack. Stehen die drei Punkte oder auch Ellipse links von Args, wird das Parameter Pack gepackt, rechts davon, wird es entpackt. Insbesondere bewirken die drei Punkte in Zeile 9 std::forward<Args>(args)..., dass auf jedes der Konstruktorargumente Perfect Forwarding angewandt wird. Das Ergebnis ist beeindruckend. Nun kann die perfekte Fabrikfunktion create zusätzlich noch mit keinem (Zeile 40) oder drei Argumenten (Zeile 43) aufgerufen werden.
Wie geht's weiter?
RAII oder auch Resource Acquisition Is Initialization ist sicher nicht das schönste aller Akronyme in C++. Was dahinter steckt, zeige ich im nächsten Artikel.
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.
Weiterlesen...