Mehr Details zur Formatierung benutzerdefinierter Datentypen in C++20

Die Implementierung eines Formatierers für einen benutzerdefinierten Datentyp mit mehr als einem Wert in C++20 ist eine Herausforderung.

In Pocket speichern vorlesen Druckansicht 3 Kommentare lesen
Schild an Gabelung

(Bild: Piyawat Nandeenopparit / Shutterstock.com)

Lesezeit: 2 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Dieser Artikel ist der fünfte in meiner Miniserie über Formatierung in C++20. Hier finden sich die vorherigen Beiträge:

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Point ist eine Klasse mit drei Mitgliedern.

// formatPoint.cpp

#include <format>
#include <iostream>
#include <string>

struct Point {
    int x{2017};
    int y{2020};
    int z{2023};
};

template <>
struct std::formatter<Point> : std::formatter<std::string> {
    auto format(Point point, format_context& context) const {
        return formatter<string>::format(
               std::format("({}, {}, {})", 
                           point.x, point.y, point.y), context);
  }
};

int main() {

    std::cout << '\n';

    Point point;

    std::cout << std::format("{:*<25}", point) << '\n';    // (1)
    std::cout << std::format("{:*^25}", point) << '\n';    // (2)
    std::cout << std::format("{:*>25}", point) << '\n';    // (3)

    std::cout << '\n';

    std::cout << std::format("{} {} {}", point.x, point.y, point.z)
              << '\n';                                     // (4)
    std::cout << std::format("{0:*<10} {0:*^10} {0:*>10}", point.x)
              << '\n';                                     // (5)

    std::cout << '\n';

}

In diesem Fall leite ich von dem Standardformatierer std::formatter<std::string> ab. Eine std::string_view ist ebenfalls möglich. std::formatter<Point> erzeugt die formatierte Ausgabe durch den Aufruf von format auf std::formatter. Dieser Funktionsaufruf erhält bereits einen formatierten String als Wert. Folglich sind alle Formatspezifikationen von std::string anwendbar (1 - 3). Im Gegenteil dazu lässt sich auch jeder Wert von Point formatieren. Genau das geschieht in (4) und (5).

Die Formatierungsfunktionen std::format* und std::vformat* haben Überladungen, die auch Locals akzeptieren. Mit diesen Überladungen kann man einen Formatierungsstring lokalisieren.

Der folgende Codeschnipsel zeigt die entsprechende Überladung von std::format:

template< class... Args >
std::string format( const std::locale& loc,
                    std::format_string<Args...> fmt, 
                    Args&&... args );

Um ein bestimmtes Local zu verwenden, gibt man L vor dem Datentyp im Formatstring an. Nun wendet man die Locale bei jedem Aufruf von std::format an oder setzt sie global mit std::locale::global.

Im folgenden Beispiel wende ich bei jedem std::format-Aufruf explizit das deutsche Local an.

// internationalization.cpp

#include <chrono>
#include <exception>
#include <iostream>
#include <thread>

std::locale createLocale(const std::string& localString) {  // (1)
  try {
    return std::locale{localString};       
  }
  catch (const std::exception& e) {
    return std::locale{""};
  }
}

int main() {

    std::cout << '\n';

    using namespace std::literals;

    std::locale loc = createLocale("de_DE");

    std::cout << "Default locale: " << std::format("{:}", 2023) 
              << '\n';
    std::cout << "German locale:  " 
              << std::format(loc, "{:L}", 2023) << '\n';    // (2)

    std::cout << '\n';

    std::cout << "Default locale: " << std::format("{:}", 2023.05)
              << '\n';
    std::cout << "German locale:  " 
              << std::format(loc, "{:L}", 2023.05) << '\n'; // (3)

    std::cout << '\n';

    auto start = std::chrono::steady_clock::now();
    std::this_thread::sleep_for(33ms);
    auto end = std::chrono::steady_clock::now();

    const auto duration = end - start;

    std::cout << "Default locale: " 
              << std::format("{:}", duration) << '\n';
    std::cout << "German locale:  " 
              << std::format(loc, "{:L}", duration) << '\n'; // (4)

    std::cout << '\n';

    const auto now = std::chrono::system_clock::now();
    std::cout << "Default locale: " << std::format("{}\n", now);
    std::cout << "German locale:  " 
              << std::format(loc, "{:L}\n", now);            // (5)

    std::cout << '\n';

}

Die Funktion createLocale (1) erstellt das deutsche Local. Wenn dies fehlschlägt, gibt sie das Standardlocal zurück, das die amerikanische Formatierung verwendet. Ich verwende das deutsche Local in (2), (3), (4) und (5). Um den Unterschied zu sehen, habe ich auch die std::format-Aufrufe direkt im Anschluss daran angewendet. Folglich wird für den ganzzahligen Wert (2) das ortsabhängige Tausendertrennzeichen und für den Fließkommawert (3) das ortsabhängige Dezimalpunkt- und Tausendertrennzeichen verwendet. Dementsprechend verwenden die Zeitdauer (4) und der Zeitpunkt (5) das angegebene deutsche Gebietsschema.

Der folgende Screenshot zeigt die Ausgabe des Programms.

std::formatter und seine Spezialisierungen definieren die Formatspezifikation auch für die Datentypen der chrono Bibliothek. Bevor ich über sie schreibe, werde ich tiefer in die Chrono-Erweiterung von C++20 eintauchen.

(rme)