Die Formatierungsbibliothek in C++20: Formatieren benutzerdefinierter Datentypen
Neben den Basistypen und std::string lassen sich in C++20 auch benutzerdefinierte Typen formatieren.

(Bild: Sinart Creative / Shutterstock.com)
- Rainer Grimm
In meinen vorangegangenen Artikeln habe ich Basistypen und std::string
formatiert:
- Softwareentwicklung: Die Formatierungsbibliothek in C++20
- Die Formatierungsbibliothek in C++20: Der Formatstring
- Die Formatierungsbibliothek in C++20: Details zum Formatstring
Nun widme ich mich dem Formatieren benutzerdefinierter Typen.
std::formatter
ermöglicht es, benutzerdefinierte Datentypen zu formatieren. Dazu muss man die Klasse std::formatter
fĂĽr den benutzerdefinierten Typ spezialisieren. Insbesondere mĂĽssen die Mitgliedsfunktionen parse
und format
implementiert werden.
parse
: Diese Funktion parst den Formatstring und gibt im Falle eines Fehlers einenstd::format_error
aus.
Die Funktion parse
sollte constexpr
sein, um das Parsen zur Compilezeit zu ermöglichen. Sie akzeptiert einen Parse-Kontext (std::format_parse_context
) und sollte das letzte Zeichen des Formatspezifizierers zurĂĽckgeben (die schlieĂźende geschweifte Klammer }
). Verzichtet man auf den Einsatz des Formatspezifizierers, ist dies auch das erste Zeichen des Formatspezifizierers.
Die folgenden Zeilen zeigen ein paar Beispiele fĂĽr das erste Zeichen des Formatspezifizierers:
"{}" // context.begin() points to }
"{:}" // context.begin() points to }
"{0:d}" // context.begin() points to d}
"{:5.3f}" // context.begin() points to: 5.3f}
"{:x}" // context.begin() points to x}
"{0} {0}" // context.begin() points to: "} {0}"
context.begin()
zeigt auf das erste Zeichen des Formatbezeichners und context.end()
auf das letzte Zeichen des gesamten Formatstrings. Gibt man keinen Formatbezeichner an, muss man alles zwischen context.begin()
und context.end()
parsen und die Position der abschlieĂźenden }
zurĂĽckgeben.
format
: Diese Funktion sollte konstant sein. Sie erhält den Wertval
und den Formatkontextcontext
.format
, formatiert den Wertval
und schreibt ihn entsprechend dem geparsten Format incontext.out()
. Der RĂĽckgabewert voncontext.out()
kann direkt instd::format_to
eingegeben werden.std::format_to
muss die neue Position fĂĽr die weitere Ausgabe zurĂĽckgeben. Es gibt einen Iterator zurĂĽck, der das Ende der Ausgabe darstellt.
So viel zur Theorie. Nun möchte ich anhand von Beispielen zeigen, wie sie sich anwenden lässt.
Ein Formatierer fĂĽr einen einzelnen Wert
// formatSingleValue.cpp
#include <format>
#include <iostream>
class SingleValue { // (1)
public:
SingleValue() = default;
explicit SingleValue(int s): singleValue{s} {}
int getValue() const { // (2)
return singleValue;
}
private:
int singleValue{};
};
template<> // (3)
struct std::formatter<SingleValue> {
constexpr auto parse(std::format_parse_context& context) { // (4)
return context.begin();
}
auto format(const SingleValue& sVal, std::format_context& context) const { // (5)
return std::format_to(context.out(), "{}", sVal.getValue());
}
};
int main() {
std::cout << '\n';
SingleValue sVal0;
SingleValue sVal2020{2020};
SingleValue sVal2023{2023};
std::cout << std::format("Single Value: {} {} {}\n", sVal0, sVal2020, sVal2023);
std::cout << std::format("Single Value: {1} {1} {1}\n", sVal0, sVal2020, sVal2023);
std::cout << std::format("Single Value: {2} {1} {0}\n", sVal0, sVal2020, sVal2023);
std::cout << '\n';
}
SingleValue
(Zeile 1) ist eine Klasse, die nur einen Wert hat. Die Mitgliedsfunktion getValue
(Zeile 2) gibt diesen Wert zurĂĽck. Ich spezialisiere std::formatter
(Zeile 3) fĂĽr SingleValue
. Diese Spezialisierung besitzt die Funktionen parse
(Zeile 4) und format
(Zeile 5). parse
gibt das Ende der Formatspezifikation zurĂĽck. Das Ende der Formatspezifikation ist die abschlieĂźende }. format
formatiert den Wert, und context.out
erstellt ein Objekt, das an std::format_to
ĂĽbergeben wird. format
gibt die neue Position fĂĽr die weitere Ausgabe zurĂĽck.
Das AusfĂĽhren dieses Programms liefert das erwartete Ergebnis:
Dieser Formatierer hat jedoch einen gravierenden Nachteil: Er unterstützt keine Formatspezifikation. Das werde ich im nächsten Beispiel verbessern.
Ein Formatierer, der einen Formatspezifikator unterstĂĽtzt
Die Implementierung eines Formatierers für einen benutzerdefinierten Datentyp ist ziemlich einfach, wenn man für den Formatierer einen Standardformatierer verwendet. Es gibt zwei Möglichkeiten, einen Standardformatierer zu verwenden: Delegation und Vererbung.
Delegation
Der folgende Formatierer delegiert seine Aufgabe an einen Standardformatierer.
// formatSingleValueDelegation.cpp
#include <format>
#include <iostream>
class SingleValue {
public:
SingleValue() = default;
explicit SingleValue(int s): singleValue{s} {}
int getValue() const {
return singleValue;
}
private:
int singleValue{};
};
template<> // (1)
struct std::formatter<SingleValue> {
std::formatter<int> formatter; // (2)
constexpr auto parse(std::format_parse_context& context) {
return formatter.parse(context); // (3)
}
auto format(const SingleValue& singleValue, std::format_context& context) const {
return formatter.format(singleValue.getValue(), context); // (4)
}
};
int main() {
std::cout << '\n';
SingleValue singleValue0;
SingleValue singleValue2020{2020};
SingleValue singleValue2023{2023};
std::cout << std::format("{:*<10}", singleValue0) << '\n';
std::cout << std::format("{:*^10}", singleValue2020) << '\n';
std::cout << std::format("{:*>10}", singleValue2023) << '\n';
std::cout << '\n';
}
std::formatter<SingleValue>
(Zeile 1) hat einen Standardformatierer fĂĽr int: std::formatter<int> formatter
(Zeile 2). Ich delegiere den Parsing-Auftrag an den Formatter (Zeile 3). Dementsprechend wird auch der Formatierungsauftrag an den Formatierer delegiert (Zeile 4).
Die Ausgabe des Programms zeigt, dass der Formatter FĂĽllzeichen und Ausrichtung unterstĂĽtzt.
Vererbung
Dank der Vererbung gelingt die Implementierung des Formatierers fĂĽr den benutzerdefinierten Datentyp SingleValue
ganz leicht.
// formatSingleValueInheritance.cpp
#include <format>
#include <iostream>
class SingleValue {
public:
SingleValue() = default;
explicit SingleValue(int s): singleValue{s} {}
int getValue() const {
return singleValue;
}
private:
int singleValue{};
};
template<>
struct std::formatter<SingleValue> : std::formatter<int> { // (1)
auto format(const SingleValue& singleValue, std::format_context& context) const {
return std::formatter<int>::format(singleValue.getValue(), context);
}
};
int main() {
std::cout << '\n';
SingleValue singleValue0;
SingleValue singleValue2020{2020};
SingleValue singleValue2023{2023};
std::cout << std::format("{:*<10}", singleValue0) << '\n';
std::cout << std::format("{:*^10}", singleValue2020) << '\n';
std::cout << std::format("{:*>10}", singleValue2023) << '\n';
std::cout << '\n';
}
Ich leite std::formatter<SingleValue>
von std::formatter<int>
ab (Zeile 1). Nur die Formatfunktion muss implementiert werden. Die Ausgabe dieses Programms ist identisch mit der Ausgabe des vorherigen Programms formatSingleValueDelegation.cpp
.
Das Delegieren an einen Standardformatierer oder das Erben von einem solchen ist ein einfacher Weg, um einen benutzerdefinierten Formatierer zu implementieren. Diese Strategie funktioniert nur fĂĽr benutzerdefinierte Datentypen, die einen Wert haben.
Wie geht's weiter?
In meinem nächsten Beitrag werde ich einen Formatter für einen benutzerdefinierten Datentyp mit mehr als einem Wert implementieren. (map)