constexpr Funktionen

Inhaltsverzeichnis[Anzeigen]

constexpr Funktionen sind Funktionen, die zur Compilezeit ausgeführt werden können. Hört sich erst mal nicht so spannend an. Ist es aber, denn durch constexpr Funktionen können Berechnungen auf die Compilezeit verschoben werden. Damit stehen deren Ergebnisse als Konstanten im ROM zur Laufzeit zur Verfügung. Darüber hinaus sind constexpr Funktionen implizit inline.

Mit C++14 wurde die Syntax für konstante Ausdrücke in der Form von constexpr Funktionen deutlich erweitert. Aus der Positiv- wurde eine Negativliste. Musste sich der Entwickler in C++11 noch die wenigen Feature merken, die in constexpr Funktinon erlaubt sind, reicht es für ihn in C++14 aus, die wenigen Feature im Gedächtnis zu behalten, die nicht erlaubt sind.

C++11

Für constexpr Funktionen gelten einige Einschränkungen:

  • Sie darf nicht virtuell sein.
  • Sowohl die Argumente als auch der Rückgabewert müssen selbst konstante Ausdrücke sein.

Weiter geht es mit dem Funktionskörper. Für diesen gilt im Wesentlichen, dass er 

  • entweder als default oder delete definiert ist oder
  • nur genau eine Rückgabeanweisung enthält.

Lässt sich damit eine vernünftige Funktion schreiben? Ja, das zeigen die constexpr Funktionen aus dem Artikel Konstante Audrücke mit constexpr. Lediglich die Funktion getAverageDistance setzt den C++14-Standard voraus.

Glücklicherweise gibt es den ternären Operator und Rekursion in C++, so dass ich den ggt-Algorithmus als constexpr Funktion implementieren kann. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// constexpr11.cpp

#include <iostream>

constexpr int gcd(int a, int b){
  return (b== 0) ? a : gcd(b, a % b);
}

int main(){
  
  std::cout << std::endl;
  
  constexpr int i= gcd(11,121);
  
  int a= 11;
  int b= 121;
  int j= gcd(a,b);

  std::cout << "gcd(11,121): " << i << std::endl;
  std::cout << "gcd(a,b): " << j << std::endl;
  
  std::cout << std::endl;
 
}

 

In Zeile 6 ist der Euklid-Algorithmus mit der Hilfe des ternären Operators und Rekursion implementiert. Verständlicher Code schaut anders aus. Natürlich kann die gcd-Funktion auch mit Argumenten gefüttert werden, die keine konstanten Ausdrücke sind (Zeile 15 und 16). Dann wird das Ergebnis konsequenterweise zur Laufzeit berechnet und kann nur durch einen nicht-konstanten Ausdruck (Zeile 17) angenommen werden.

Das Ergebnis des Programms ich nicht so spannend.

 constexpr11

Einen genaueren Blick will ich aber gerne noch auf die erzeugten Assembleranweisungen des Programms werfen. Die sind sehr aufschlussreich, und setzen keine tiefen Assemblerkenntnisse voraus.

 constexpr11Objectdump

Der Aufruf der constexpr Funktion in Zeile 13 im Sourcecode führt dazu, dass der Wert als Konstante 0xb bereits im Objektcode zur Verfügung steht. Dies steht im Gegensatz zu dem Funktionsaufruf gcd(a,b) in Zeile 17. In diesem Fall muss der Prozessor erst die Variablen auf den Funktionsstack schieben (Anweisungen 400939 - 400941 im Objektdump), anschließend die Funktion aufrufen (callq) und das Ergebnis des Funktionsaufrufs in der Variable j speichern (400948).

Natürlich lassen sich noch mächtigere constexpr Funktionen definieren, indem ternären Operatoren ineinander verschachtelt werden. Das muss aber nicht sein. Mit C++14 folgen constexpr Funktionen fast der gewohnten C++-Syntax.

C++14

constexpr Funktionen können

  • bedingte Sprung- und Iterationsanweisungen enthalten.
  • mehrere Anweisungen enthalten.
  • constexpr Funktionen aufrufen.
  • fundamentale Datentypen verwenden, die mit einem konstanten Ausdruck initialisiert werden müssen.

Dem entgegen steht, das constexpr Funktionen in C++14 keine statischen oder thread_local Daten verwenden dürfen. Auch ein try-Block oder eine goto-Anweisungen ist nicht erlaubt. Mit diesen Erweiterungen, ist der ggt-Algorithmus deutlich einfacher in C++14 zu implementieren.

 

 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
// constexpr14.cpp

#include <iostream>

constexpr auto gcd(int a, int b){
  while (b != 0){
    auto t= b;
    b= a % b;
    a= t;
  }
  return a;
}

int main(){
  
 std::cout << std::endl;
  
  constexpr int i= gcd(11,121);
  
  int a= 11;
  int b= 121;
  int j= gcd(a,b);

  std::cout << "gcd(11,121): " << i << std::endl;
  std::cout << "gcd(a,b): " << j << std::endl;
  
  std::cout << std::endl;
 
}

 

Auf die Ausgabe des Programms verzichtet ich, das sie identisch ist mit der Ausgabe der C++11 Variante des ggt-Algorithmus.

Dank einer Diskussion mit Odin Holmes auf der Facebookgruppe Modernes C++ möchte ich noch ein sehr interessanten Anwendungsfall für constexpr Funktionen vorstellen. 

Reine Funktionen

constexpr Funktionen können auch zur Laufzeit ausgeführt werden. Wird der Rückgabewert einer constexpr Funktion von einer constexpr Variable oder Objekt angenommen, wird die Funktion zur Compilezeit ausgeführt. Welchen Grund kann es nun geben, eine constexpr Funktion zur Laufzeit auszuführen? Der naheliegendste Grund ist sicher, dass die Funktion schon als constexpr Funktion vorliegt. Warum sollte sie daher nicht auch zu Laufzeit ausgeführt werden. Es gibt aber einen viel überzeugenderes Argument dafür, constexpr Funktionen zur Laufzeit auszuführen,

Eine constexpr Funktion kann potentiell zur Compilezeit ausgeführt werden. Zur Compilezeit gibt es keinen Zustand. Hier befinden wir uns in einer rein funktionalen Subsprache der imperativen Programmiersprache C++. Insbesondere bedeutet dies, das zur Compilezeit ausgeführte constexpr Funktionen reine Funktionen sein müssen. Wird nun dieser Funktionsaufruf zur Laufzeit ausgeführt, indem das Ergebnis ohne constexpr angenommen wird, bleibt der Funktionsaufruf rein. Reine Funktionen sind Funktionen, die immer den gleichen Wert zurückgeben, wenn sie mit den gleichen Argumenten aufgerufen werden. Reine Funktionen verhalten sich wie unendlich große Tabellen, in denen der Wert einfach nur nachgeschlagen wird. Diese Zusicherung, dass ein Ausdruck immer den gleichen Wert zurückgibt, wenn er mit dem gleichen Argumenten bedient wird, nennt sich Referenzielle Transparenz.

Reine Funktionen haben viele Vorteile:

  • Der Funktionsaufruf kann durch sein Ergebnis ersetzt werden.
  • Die Ausführung von reine Funktionen kann automatisch auf andere Threads verteilt werden.
  • Funktionsaufrufe können umsortiert werden.
  • Sie können einfach refaktoriert werden.

Die letzten drei Punkte gelten, da reine Funktionen keinen Programmzustand verändern und damit von diesem nicht abhängen. Gerne werden reine Funktionen auch mathematische Funktionen genannt.

Es gibt daher viele Gründe, constexpr Funktionen zu verwenden. Die Tabelle bringt nochmals die Unterschiede von reinen und unreinen Funktionen auf den Punkt.

ReinVersuUnrein

Wer mehr über die Charakteristiken der funktionalen Programmierung wissen will, den verweise ich gerne auf meinen frei zugänglichen Artikel Grundzüge der funktionalen Programmierung für das Linux-Magazin Online.

Rein und Reiner

Ich will es gerne noch explizit betonen. constexpr Funktionen sind nicht per se rein. Vielen Dank an Herrn Marcel Wid, der mich darauf aufmerksam gemacht hat. Sie sind nur reiner als gewöhnliche Funktionen, da in ihnen zum Beispiel nur Funktionen verwendet werden können, die selbst constexpr Funktionen sind. Wer nur reine Funktionen verwenden will, dem lege ich das Studium der rein funktionalen Programmiersprache Haskell ans Herz. Mit dem sehr lesenswerten Buch Learn You a Haskell For Great Good von Miran Lipovaca gibt es die ideale Einstiegslektüre, die dazu auch noch online zur Verfügung steht. 

Wie geht's weiter?

In dem Artikel Immer Sicher habe ich den ggt-Algorithmus immer weiter optimiert, damit er typsicherer wird. Dank der Funktion static_assert und den Funktionen der Type-Traits Bibliothek gelang dies relativ beeindruckend. Doch das ist noch nicht die ganze Geschichte zu der Type-Traits Bibliothek. Dank ihrer Introspektionsfähigkeit zur Compilezeit ist es möglich, sich selbst optimierende Programme zu schreiben. Wie? Das zeigt der nächste Artikel.

Was du schon immer wissen wolltest

Die Charakteristiken der funktionalen Programmierung stelle ich in meinen Artikel Grundzüge der funktionalen Programmierung für das Linux-Magazin Online genauer vor.

Für das Linux-Magazin habe ich das Buch Learn You A Haskell for Great Good von Miran Lipovaca rezensiert. Hier geht es direkt zur Rezension eines meiner Lieblingsbücher.

 

 

 

 

 

 

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.

 

Kommentar schreiben


Abonniere den Newsletter (+ pdf Päckchen)

Beiträge-Archiv

Sourcecode

Neuste Kommentare