Wird eine Variable als constexpr definiert, kann sie zur Compilezeit evaluiert werden. Dies gilt nicht nur für Variablen von fundamentalen Datentypen, sondern auch für Instanzen von benutzerdefinierten Datentypen (Objekte). Insbesondere bei Objekten gibt es deutliche Einschränkungen, damit diese zur Compilezeit evaluiert werden können.
Um einfacher fundamentale Datentypen von restlichen Datentypen unterscheiden zu können, verwende ich für fundamentale Datentypen wie bool, char, int und double den Begriff Variable. Mit benutzerdefinierten Datentypen beziehe ich mich auf die restlichen Datentypen. Das sind Datentypen wie std::string, die die C++-Bibliothek mitbringt oder Datentypen, die der Anwender explizit definiert. Benutzerdefinierte Datentypen verwenden typischerweise fundamentale Datentypen.
Variablen
Durch Voranstellen des Schlüsselwortes constexpr wird eine Variable zum konstanten Ausdruck.
constexpr double myDouble= 5.2;
Damit kann die Variable in Kontexten verwendet werden, die konstante Ausdrücke voraussetzen. Dies ist zum Beispiel der Fall, wenn die Größe eines Arrays bestimmt werden soll. Das ist nur zur Compilezeit möglich.
Für die Deklaration von constexpr Variablen gilt es ein paar Regeln im Kopf zu behalten.
Die Variable
- ist implizit const.
- muss initialisiert werden.
- benötigt einen konstanten Ausdruck zum Initialisieren.
Die Regeln sind einleuchtend. Wird eine Variable zur Compilezeit evaluiert, kann sie nur von Werten abhängen, die auch zu Compilezeit ermittelt werden können.
Objekte werden durch den Aufruf des Konstruktors einer Klasse erzeugt. Für diesen Konstruktor gelten besondere Regeln.
Benutzerdefinierte Typen
Die Klasse MyDistance aus dem Artikel Konstante Ausdrücke mit constexpr erfüllt alle Bedingungen, um seine Objekte zur Compilezeit zur erzeugen. Wie lauten die wesentlichen Bedingungen?
Ein constexpr Konstruktor
- kann nur mit konstanten Ausdrücken aufgerufen werden.
- darf keine virtuelle Basisklassen besitzen.
- kann keine Ausnahmebehandlung verwenden.
- muss als default oder delete erklärt worden sein oder einen leeren Funktionskörper besitzen.
- Jede Basisklasse oder auch jedes nicht-statische Mitglied muss direkt in der Initialisierungsliste des Konstruktors oder direkt in der Klassendefinion initialisiert werden. Natürlich gilt konsequenterweise, dass zum einen jeder verwendete Konstruktor selbst ein constexpr Konstruktor ist, als auch zum anderen, das die verwendeten Initialisierer konstante Ausdrücke sind.
Sorry, aber es ging nicht einfacher. Um die Theorie anschaulicher zu gestalten, definiere ich eine Klasse MyInt, die die meisten Punkte der Aufzählung veranschaulicht. Die Klasse besitzt darüber hinaus constexpr Methoden. Für constexpr Methoden und Funktionen gelten besondere Regeln. Diese folgen aber im nächsten Artikel, um den Blick nun auf das Wesentliche zu lenken.
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
|
// userdefinedTypes.cpp
#include <iostream>
#include <ostream>
class MyInt{
public:
constexpr MyInt()= default;
constexpr MyInt(int fir, int sec): myVal1(fir), myVal2(sec){}
MyInt(int i){
myVal1= i-2;
myVal2= i+3;
}
constexpr MyInt(const MyInt& oth)= default;
constexpr MyInt(MyInt&& oth)= delete;
constexpr int getSum(){ return myVal1+myVal2; }
friend std::ostream& operator<< (std::ostream &out, const MyInt& myInt){
out << "(" << myInt.myVal1 << "," << myInt.myVal2 << ")";
return out;
}
private:
int myVal1= 1998;
int myVal2= 2003;
};
int main(){
std::cout << std::endl;
constexpr MyInt myIntConst1;
MyInt myInt2;
constexpr int sec= 2014;
constexpr MyInt myIntConst3(2011,sec);
std::cout << "myIntConst3.getSum(): " << myIntConst3.getSum() << std::endl;
std::cout << std::endl;
int a= 1998;
int b= 2003;
MyInt myInt4(a,b);
std::cout << "myInt4.getSum(): " << myInt4.getSum() << std::endl;
std::cout << myInt4 << std::endl;
std::cout << std::endl;
// constexpr MyInt myIntConst5(2000); ERROR
MyInt myInt6(2000);
std::cout << "myInt6.getSum(): " << myInt4.getSum() << std::endl;
std::cout << myInt6 << std::endl;
// constexpr MyInt myInt7(myInt4); ERROR
constexpr MyInt myInt8(myIntConst3);
std::cout << std::endl;
int arr[myIntConst3.getSum()];
static_assert( myIntConst3.getSum() == 4025, "2011 + 2014 should be 4025" );
}
|
Zuerst zur Klasse MyInt. MyInt besitzt drei Konstruktoren. Einen constexpr Default-Konstruktor (Zeile 8) und jeweils einen Konstruktor, der zwei Argumente(Zeile 9) bzw. ein Argument (Zeile 10) benötigt. Der Konstruktor mit zwei Argumenten ist ein constexpr Konstruktor. Daher ist sein Körper leer. Dies gilt nicht für den Konstruktor mit einem Argument, der nicht constexpr deklariert ist. Weiter geht es mit einem durch den Compiler erzeugten Copy-Konstruktor und einen Move-Konstruktor, der auf delete gesetzt ist (Nummer 4 der Aufzählung). Darüber hinaus besitzt die Klasse noch zwei Methoden. Aber nur die Methode getSum ist ein konstanter Ausdruck. Die Variablen myVal1 und myVal2 in Zeile 26 und 27 können nur auf zwei Arten initialisiert werden, wenn sie von einem constexpr Objekt verwendet werden wollen. Entweder müssen sie, wie in Nummer 5 der Aufzählung beschrieben, in der Initialisierungsliste des Konstruktors (Zeile 9) oder im Klassenkörper direkt (Zeile 26 und Zeile 27) initialisiert werden. Die Variablen im Körper der Konstruktors zu initialisieren (Zeile 11 und 12) ist für ein constexpr Konstruktor (Nummer 4) nicht möglich.
Um die main-Funktion besser zu verstehen, hilft die Ausgabe des Programms.
Ein paar Besonderheiten zeigt das Beispiel.
- Ein constexpr Konstruktor kann zur Laufzeit verwendet werden. Das resultierende Objekt darf konsequenterweise nicht als constexpr deklariert werden. (Zeile 36 und Zeile 46)
- Wird ein nicht konstanter Ausdruck als constexpr deklariert, quittiert dies der Compiler mit einer Fehlermeldung. (Zeile 52 und 57)
- constexpr Konstruktoren können mit nicht constexpr Konstruktoren koexistieren. Dies gilt natürlich auch für die Methoden der Klasse.
Die entscheidende Beobachtung ist, das ein durch einen constexpr Konstruktor erzeugtes Objekt nur Methoden aufrufen kann, die als constexpr deklariert sind.
Doch halt. Was sollen die unmotivierten zwei letzten Zeilen 62 und 63 in der main-Funktion?
Der Beweis
Ganz einfach. Sie sind der doppelte Beweis dafür, das der Aufruf der Methode myIntConst3.getSum() zur Compilezeit ausgeführt wird.
Zum einen erlaubt es die C++-Syntax nur, die Größe von Arrays mit konstanten Ausdrücken anzugeben. Zum andern wertet static_assert zur Compilezeit seinen Ausdruck aus. Kann er dies nicht, gibt er eine eindeutige Fehlermeldung beim Compilieren aus.
Ersetze ich daher die Codezeile 63
static_assert( myIntConst3.getSum() == 4025, "2011 + 2014 should be 4025" );
durch den Ausdruck
static_assert( myIntConst4.getSum() == 4001, "1998 + 2003 should be 4001" );
moniert dies mein GCC postwendend:
Was du immer schon wissen wolltest
In dem Artikel Methodik mit Methode 08/2014 für das Linux-Magazin erkäre ich die Details zu den kontextsensitiven Schlüsselworten default und delete.
Wie geht's weiter
Ich denke, du weißt es bereits. Im nächsten Artikel schaue ich mir constexpr Funktionen genauer an. Diese besitzen mit C++11 einige Einschränkungen, die fast alle in C++14 nicht mehr gelten. constexpr Funktionen in C++14 verhalten sich fast wie normale Funktionen. Natürlich schließt meine Argumentation für Funktionen auch die für die Methoden einer Klasse ein.
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...