Benutzerdefinierte Literale

Inhaltsverzeichnis[Anzeigen]

Benutzerdefinierte Literale in C++ stellen ein einmaliges Feature unter allen Mainstream-Programmiersprachen dar. Erlauben sie es doch, Werte direkt mit Einheiten zu verknüpfen.

Die Syntax

Literale repräsentieren explizite Werte in einem Programm. Dies kann der Wahrheitswert true, die Zahl 3 oder 4.15, dies können aber auch das Zeichen 'a' oder der C-String "hallo" sein. Selbst die Lambda-Funktion [](int a, int b){ return a+b; } ist ein sogenanntes Funktions-Literal. Mit C++ ist es möglich, aus built-in Literale für natürliche Zahlen, Fließkommazahlen, Zeichen und C-Strings durch Anhängen eines Suffix ein benutzerdefiniertes Literal zu erzeugen. 

Für die benutzerdefinierten Literale gilt die folgende Syntax: built-in Literal + _ + Suffix.

Typischerweise werden Suffixe für Einheiten verwendet:

101000101_b
63_s
10345.5_dm
123.45_km
100_m
131094_cm
33_cent
"Hallo"_i18n

 

Was ist nun die entscheidende Idee der benutzerdefinierten Literalen? Der C++-Compiler bildet die benutzerdefinierten Literale auf die entsprechenden Literal-Opertoren ab. Dieser Literal Operator muss natürlich vom Programmierer implementiert werden.

Die Magie ist am einfachsten an dem benutzerdefinierten Literal 0101001000_b erklärt, das für einen binären Wert steht. Der C++-Compiler bildet dieses benutzerdefiniertes Literal 0101001000_b auf den Literal-Operator operator"" _b(long long int bin) ab. Ein paar Besonderheiten der benutzerdefinierten Literale und Literal-Operatoren fehlen noch.

  • Zwischen den doppelten Anführungszeiten ("") und dem Unterstrich mit Suffix (_b) ist eine Leerzeichen notwendig.
  • Der binäre Wert (0101001000) steht in der Variable bin im Körper des Literal-Operators zur Verfügung.
  • Findet der Compiler zu einem benutzerdefinierten Literal keinen Literal-Operator, steigt er mit einer Fehlermeldung aus.

Mit C++14 gibt es die neue, alternative Syntax für benutzerdefinierte Literale. Diese unterscheidet sich von der vorgestellten C++11-Syntax dadurch, dass sie keine Leerzeichen besitzt. Dadurch ist es möglich, reservierte Bezeichner wie _C als Suffix zu verwenden.  So ist zum Beispiel 11_C als benutzerdefiniertes Literal erlaubt. Im konkreten Fall wird 11_C auf den Literal-Operator""_C(unsigned long long int) abgebildet. Die einfache Regel ist es, dass ein Suffix nun mit einem Grossbuchstabe beginnen kann.

Benutzerdefinierten Literale sind das Killerfeature in modernem C++, wenn es darum geht, sicherheitskritische Software zu schreiben. Warum? Durch die automatische Abbildung des benutzerdefinierten Literals auf seinen Literal-Operator ist es möglich, typsichere Arithmethik zu betreiben. Die Compiler sorgt automatisch dafür, dass keine Äpfel mit Birnen verrechnet werden. Beispiel gefällig?

Wie viele Meter fahre ich im Schnitt als Vielfahrer in der Woche? Diese Frage hat mich schon öfters beschäftigt.

Typsicheres Rechnen mit Entfernungen

Bevor ich auf die Details eingehe, zuerst das Hauptprogramm. 

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

#include <distance.h>
#include <unit.h>

using namespace Distance::Unit;

int main(){

  std:: cout << std::endl;

  std::cout << "1.0_km: " << 1.0_km << std::endl;
  std::cout << "1.0_m: " << 1.0_m << std::endl;
  std::cout << "1.0_dm: " << 1.0_dm << std::endl;
  std::cout << "1.0_cm: " << 1.0_cm << std::endl;
  
  std::cout << std::endl;

  std::cout << "0.001 * 1.0_km: " << 0.001 * 1.0_km << std::endl;
  std::cout << "10 * 1_dm: " << 10 * 1.0_dm << std::endl;
  std::cout << "100 * 1.0cm: " << 100 * 1.0_cm << std::endl;
  std::cout << "1_km / 1000: " << 1.0_km / 1000 << std::endl;

  std::cout << std::endl;
  std::cout << "1.0_km + 2.0_dm +  3.0_dm + 4.0_cm: " << 1.0_km + 2.0_dm +  3.0_dm + 4.0_cm << std::endl;
  std::cout << std::endl;
  
  auto work= 63.0_km;
  auto workPerDay= 2 * work;
  auto abbrevationToWork= 5400.0_m;
  auto workout= 2 * 1600.0_m;
  auto shopping= 2 * 1200.0_m;
  
  auto distPerWeek1= 4*workPerDay-3*abbrevationToWork+ workout+ shopping;
  auto distPerWeek2= 4*workPerDay-3*abbrevationToWork+ 2*workout;
  auto distPerWeek3= 4*workout + 2*shopping;
  auto distPerWeek4= 5*workout + shopping;

  std::cout << "distPerWeek1: " << distPerWeek1 << std::endl;
  
  auto averageDistance= getAverageDistance({distPerWeek1,distPerWeek2,distPerWeek3,distPerWeek4});
  std::cout<< "averageDistance: " << averageDistance << std::endl;
  
  std::cout << std::endl;

}

 

Die Literal-Operatoren sind im Namensraum Distance::Unit implementiert. Für benutzerdefinierte Literale sollen Namensräume verwendet werden, da die Gefahr von Namenskonflikten aus zwei Gründen relativ hoch ist. Zum einen sind die Suffixe in der Regel kurz, zum anderen repräsentieren die Suffixe gerne Einheiten, für die sich schon Abkürzungen etabliert haben. So verwende ich die Suffixe km, m, dm und cm.

Nun aber zu Ausgabe des Programms. Als Grundeinheit für die Entfernung verwende ich den Meter.

average

In den Zeilen 12 - 15 gebe ich dir verschiedenen Entfernungen aus, in den Zeilen 19 - 22 berechne ich einen Meter in verschiedenen Variationen. Ein letzter Test ist sehr vielversprechend:
1.0_km + 2.0_dm +  3.0_dm + 4.0_cm  sind 1000.54 m (Zeile 54). Der Compiler sorgt dafür, dass alle Einheiten richtig verrechnet werden.

Nun gilt es. Wie viele Meter lege ich im Schnitt pro Woche zurück? Dazu definiere ich mir erst die Konstanten work, workPerDay, abbrevationToWork und shopping. Aus diesen Komponenten lassen sich einfach die Entfernungen für die 4 Wochen zusammenstellen (Zeile 34 - 37). In der ersten Woche lege ich gut 493 km zurück. Die Funktion getAverageDistance (Zeile 41) wird mit einer Initialisiererliste befüllt und ermittelt den Durchschnitt. Im Schnitt fahre ich pro Woche 255900 m. Das muss weniger werden!

Beeindruckt? Ich denke schon. Bevor ich aber auf die Details zu den Literal-Operatoren im Namensraum Distance::Unit und den MyDistance-Objekten im Namensraum Distance eingehe, möchte ich erst die Automatismen vorstellen, die bei der Arithmetik der benutzerdefinierten Literale stattfinden.

Automatismen

Eine Kleinigkeit habe ich verschwiegen. Wo kommen die MyDistance-Objekte her? Diese verbergen sich hinter der automatischen Typableitung im Beispielprogramm. So lautet der explizite Typ hinter der Variable work in dem Ausdruck auto work= 63.0_km (Zeile 28) Distance::MyDistance. Damit ist die Zeile 28 äquivalent zu Distance::MyDistance work= 63.0_km;

 

arithmetik

 

 

Wird im Sourcecode 1.5_km + 105.1_m verwendet, starten unter der Decke der Automatismus. Zum einen bildet der Compiler die Suffixe km und m auf die entsprechenden Literal-Operatoren ab, zum andern bildet der Compiler den +-Operator auf den überladen +-Operator des MyDistance-Objektes ab. Diese zwei Schritte setzten natürlich voraus, dass der Programmierer seinen Teil des Vertrages erfüllt hat. Das bedeutet in diesem konkreten Fall, er muss die Literale-Operatoren und den +-Operator für MyDistance-Objekte implementieren haben. Die durch den Compiler automatisch vollzogene Schritte sind in der Graphik durch schwarze Pfeile angedeutet. Die roten Pfeile stehen für die Funktionalität, die der Programmierer zu implementieren hat.

Was fehlt nun noch? Genau das Fleisch hinter den roten Pfeilen.

Aufgaben des Programmierers

Zuerst zum klassischen Überladen von Operatoren. Für die Klasse MyDistance habe ich die Grundrechenarten (Zeile 15 - 28)  und den Ausgabeoperator (Zeile 30 - 33) überladen. Die Operatoren sind globalen Funktionen und können dank der friend-Deklaration auf die Internas der Klasse zugreifen. In der privaten Variable m speichere ich die Entfernung. Die Funktion getAverageDistance in Zeile 41 - 45 verwendet bereits den überladenen Additions- und Divisions-Operator.

 

 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
// distance.h

#ifndef DISTANCE_H
#define DISTANCE_H

#include <iostream>
#include <ostream>


namespace Distance{
  class MyDistance{
    public:
      MyDistance(double i):m(i){}

      friend MyDistance operator+(const MyDistance& a, const MyDistance& b){
        return MyDistance(a.m + b.m);
      }
      friend MyDistance operator-(const MyDistance& a,const MyDistance& b){
        return MyDistance(a.m - b.m);
      }
	 
friend MyDistance operator*(double m, const MyDistance& a){ return MyDistance(m*a.m); } friend MyDistance operator/(const MyDistance& a, int n){ return MyDistance(a.m/n); } friend std::ostream& operator<< (std::ostream &out, const MyDistance& myDist){ out << myDist.m << " m"; return out; } private: double m; }; } Distance::MyDistance getAverageDistance(std::initializer_list<Distance::MyDistance> inList){ auto sum= Distance::MyDistance{0.0}; for (auto i: inList) sum = sum + i ; return sum/inList.size(); } #endif

 

Kürzer, aber spannender sind da schon die Literal-Operatoren.

 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
// unit.h

#ifndef UNIT_H
#define UNIT_H

#include <distance.h>

namespace Distance{

  namespace Unit{
    MyDistance operator "" _km(long double d){
      return MyDistance(1000*d);
    }
    MyDistance operator "" _m(long double m){
      return MyDistance(m);
    }
    MyDistance operator "" _dm(long double d){
      return MyDistance(d/10);
    }
    MyDistance operator "" _cm(long double c){
      return MyDistance(c/100);
    }
  }
}

#endif

 

Die Literal-Operatoren nehmen als Argument long double an und geben ein MyDistance-Objekt zurück. Die erzeugten MyDistance-Objekte werden dabei automatisch auf Meter geeicht. Und nun? Das war schon die ganze Funktionalität, die in der ToDo-Liste des Programmieres steht.

Ein großes Optimierungspotential habe ich bei meinem Programm ignoriert. Fast alle Operationen können zur Compilezeit ausgeführt, fast alle Objekte zur Compilezeit instanziiert werden. Dazu ist notwendig, dass die entsprechenden Operationen und Objekte als constexpr deklariert wrerden. Dies ist aber ein Feature, das im Artikel Konstante Ausdrücke genauer vorstelle.

Wie geht's weiter?

Benutzerdefinierte Literale gibt es nicht nur für Fließkommazahlen, sondern auch für natürliche Zahlen, Zeichen und C-Strings. Darüber hinaus existieren für natürliche Zahlen und Fließkommazahlen zwei syntaktische Formen: Cooked und raw. Es gibt noch einiges zu erzählen 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.

 

 

Kommentar schreiben


Modernes C++

Abonniere den Newsletter

Inklusive zwei Artikel meines Buchs
Introduction und Multithreading

Beiträge-Archiv

Sourcecode

Neuste Kommentare