Dieser Artikel wurde maschinell übersetzt.

Windows mit C++

Verwenden von Printf mit Modern C++

Kenny Kerr

Kenny KerrWas wäre nötig, um Printf modernisieren? Das mag wie eine seltsame Frage für viele Entwickler, die glauben, dass C++ bereits einen modernen Ersatz für Printf bietet. Zwar der Anspruch auf Ruhm der C++-Standardbibliothek zweifellos den ausgezeichneten Standard Template Library (STL), es enthält auch eine Stream-basierte Eingabe /­Ausgabebibliothek, hat keine Ähnlichkeit mit STL und keiner seiner Prinzipien verkörpert, mit Bezug zu Effizienz.

"Generische Programmierung ist ein Ansatz zur Programmierung, die auf Design-Algorithmen und Datenstrukturen konzentriert, so dass sie in die allgemeinste Einstellung ohne Verlust der Effizienz arbeiten" nach Alexander Stepanov und Daniel Rose, in dem Buch "Von Mathematik in die generische Programmierung" (Addison-Wesley Professional, 2015).

Um ehrlich zu sein, ist weder Printf noch Cout in keiner Weise repräsentativ für moderne C++. Die Printf-Funktion ist ein Beispiel für eine Variadic-Funktion und ein paar gute Verwendungen dieses etwas spröde Features, die von der Programmiersprache C geerbt. Variadic-Funktionen zurückdatieren Variadic Templates. Letztere bieten eine wirklich moderne und robuste Anlage für den Umgang mit einer Variablen Anzahl von Typen oder Argumente. Im Gegensatz dazu Cout nicht beschäftigen Variadic nichts, aber stattdessen stützt sich so stark auf virtuelle Funktionsaufrufe, dass der Compiler nicht in der Lage, viel tun, um ihre Leistung zu optimieren. Tatsächlich hat die Entwicklung des CPU-Designs Printf begünstigt, dabei wenig zur Verbesserung der Leistung des polymorphen Ansatzes der Cout. Daher möchten Sie Leistung und Effizienz, ist Printf die bessere Wahl. Es erzeugt auch Code, übersichtlicher ist. Im Folgenden finden Sie ein Beispiel:

#include <stdio.h>
int main()
{
  printf("%f\n", 123.456);
}

Die Konvertierungsspezifizierer %f sagt Printf zu erwarten eine Gleitkommazahl und wandelt es in Dezimalschreibweise. Die \n ist nur ein gewöhnlicher Zeilenendezeichen, die einen Wagenrücklauf, je nach Destination ausgeweitet werden kann. Die Gleitkomma-Konvertierung übernimmt eine Genauigkeit von 6, unter Bezugnahme auf die Anzahl der Ziffern, die nach dem Dezimalkomma angezeigt wird. Daher wird in diesem Beispiel die folgenden Zeichen, gefolgt von einer neuen Zeile gedruckt:

123.456000

Aus dem gleichen Grund mit Cout zu erreichen scheint relativ gerade­zuerst zu übermitteln:

#include <iostream>
int main()
{
  std::cout << 123.456 << std::endl;
}

Hier setzt Cout auf Operatorüberladung zum direkten oder schicken die Gleitkommazahl in den Ausgabestream geschrieben. Den Missbrauch von überladen, die auf diese Weise mag ich nicht, aber ich gebe zu, es ist eine Frage des persönlichen Stils. Schließlich schließt Endl durch Einfügen einer neuen Zeile in den Ausgabestream. Dies ist nicht ganz dasselbe wie im Beispiel Printf und erzeugt eine Ausgabe mit einer unterschiedlichen Dezimalpräzision:

123.456

Dies führt zu einer offensichtlichen Frage: Wie kann ich die Genauigkeit für die jeweiligen Abstraktionen ändern? Nun, wenn ich nur zwei Ziffern nach dem Dezimalkomma will, kann ich einfach dies als Bestandteil der Printf-Float-Punkt-Konvertierungsspezifizierer angeben:

printf("%.2f\n", 123.456);

Jetzt rundet Printf die Anzahl produzieren das folgende Ergebnis:

123.46

Um die gleiche Wirkung mit Cout zu erhalten erfordert ein bisschen mehr eingeben:

#include <iomanip> // Needed for setprecision
std::cout << std::fixed << std::setprecision(2)
          << 123.456 << std::endl;

Auch wenn Sie nicht die Ausführlichkeit von all dem nichts und lieber die Flexibilität oder Ausdruckskraft genießen, denken Sie daran, das diese Abstraktion zu Kosten kommt. Erstens sind die festen und Setprecision Manipulatoren statusbehaftete, was bedeutet, dass ihre Wirkung bleibt bestehen, bis sie rückgängig gemacht oder zurückgesetzt sind. Im Gegensatz dazu umfasst die Printf-Konvertierungsspezifizierer alle Voraussetzungen für diese single-Konvertierung ohne Beeinträchtigung anderer Code. Die anderen Kosten kann keine Rolle, für die meisten Ausgabe, aber der Tag kommen könnte, wenn Sie feststellen, dass alle anderen Programme ausgeben können oft schneller als verkaufen können. Abgesehen von den Overhead von virtuellen Funktionsaufrufe gibt Endl hinaus Ihnen mehr als Sie erwartet haben könnte. Nicht nur sendet es eine neue Zeile der Ausgabe, sondern daraus, dass des zugrunde liegende Streams, die Ausgabe zu leeren. Beim Schreiben von jeglicher Art von i/o, ob auf der Konsole, eine Datei auf einem Datenträger, eine Netzwerkverbindung oder sogar eine Grafik-Pipeline, Spülung ist in der Regel sehr teuer, und wiederholte Spülungen werden zweifellos Leistung beeinträchtigen.

Nun, ich habe erforscht und kontrastiert Printf und Cout ein wenig, ist es Zeit, auf die ursprüngliche Frage zurückzukommen: Was wäre nötig, um Printf modernisieren? Sicherlich kann mit dem Aufkommen der modernen C++, C ++ 11 und darüber hinaus, ein Beispiel ist ich die Produktivität und Zuverlässigkeit von Printf verbessern ohne Performance-Einbußen. Anderen unabhängigen etwas, dass der C++-Standardbibliothek der Programmiersprache offizielle String-Klasse angehört. Obwohl diese Klasse im Laufe der Jahre auch verleumdet wurde, bietet sie eine hervorragenden Leistung. Während nicht ohne Schuld, bietet es eine sehr nützliche Möglichkeit, Zeichenfolgen in C++ verarbeiten. Daher sollte jede Modernisierung der Printf wirklich schön mit String und Wstring zu spielen. Mal sehen, was getan werden kann. Zunächst möchte ich ansprechen, was meiner Meinung nach die meisten leidige Problem der Printf:

std::string value = "Hello";
printf("%s\n", value);

Dies sollte eigentlich funktionieren, aber wie ich sicher, dass Sie deutlich sehen können bin, stattdessen es führt in was ist liebevoll bekannt als "nicht definiertes Verhalten." Wie Sie wissen, ist Printf alles über Text und der C++-String-Klasse ist die wichtigste Manifestation des Textes in der C++-Programmiersprache. Was ich tun muss ist, wickeln Sie Printf so einer Weise, die einfach funktioniert. Ich will nicht immer wieder die Zeichenfolge Null-terminierte Array wie folgt zu pflücken:

printf("%s\n", value.c_str());

Dies ist nur langweilig, also werde ich es zu beheben, durch das Einwickeln von Printf. Traditionell hat dabei eine andere Variadic Funktion schreiben. Vielleicht so etwas wie dies:

void Print(char const * const format, ...)
{
  va_list args;
  va_start(args, format);
  vprintf(format, args);
  va_end(args);
}

Leider erhält es mir nichts. Es könnte nützlich sein, eine Printf-Variante zu wickeln, um auf einige andere Puffer schreiben, aber in diesem Fall habe ich nichts von Wert gewonnen. Ich will nicht in C-Format Variadic-Funktionen zurück. Stattdessen möchte ich nach vorne schauen und moderne C++ zu umarmen. Glücklicherweise werden dank die C ++ 11 Variadic Templates, nie habe ich eine andere Variadic-Funktion in meinem Leben zu schreiben. Anstatt Umhüllung die Printf-Funktion in einer anderen Variadic-Funktion, kann ich stattdessen in einer Vorlage Variadic umschließen:

template <typename ... Args>
void Print(char const * const format,
           Args const & ... args) noexcept
{
  printf(format, args ...);
}

Am Anfang mag es nicht, dass ich viel gewonnen habe. Würde ich die Drucken-Funktion wie folgt aufrufen:

Print("%d %d\n", 123, 456);

Es führt der Args Parameter-Pack, bestehend aus 123 und 456, um innerhalb des Körpers des Variadic Templates zu erweitern, als wenn ich einfach Folgendes geschrieben hatte:

      

printf("%d %d\n", 123, 456);

So was habe ich gewonnen? Sicher, ich rufe Vprintf, anstatt Printf und ich brauche nicht zum Verwalten einer Va_list und die zugehörige Stack -­twiddling Makros, aber ich bin immer noch lediglich Argumente weiterleiten. Don' t die Einfachheit dieser Lösung jedoch übersehen. Wieder wird der Compiler die Funktionsvorlage-Argumente auspacken, als wäre ich einfach Printf direkt, was, dass aufgerufen worden bedeutet in der Verpackung Printf auf diese Weise gibt es kein Overhead. Es bedeutet auch dies ist noch erstklassige C++ und ich kann die Sprache mächtigen Metaprogrammierung Techniken, alle erforderlichen Code zu injizieren beschäftigen — und in ganz allgemeiner Weise. Anstatt einfach erweitern das Args-Parameter-Pack, kann jedes Argument um Anpassungen von Printf benötigt hinzuzufügen umbrochen werden. Betrachten Sie diese einfache Funktion-Vorlage:

template <typename T>
T Argument(T value) noexcept
{
  return value;
}

Es scheint nicht viel zu tun und in der Tat es nicht, aber ich kann jetzt das Parameter-Pack, um jedes Argument in eine dieser Funktionen umbrochen werden wie folgt erweitern:

template <typename ... Args>
void Print(char const * const format,
           Args const & ... args) noexcept
{
  printf(format, Argument(args) ...);
}

Ich rufe noch die Drucken-Funktion auf die gleiche Weise:

Print("%d %d\n", 123, 456);

Aber es jetzt effektiv erzeugt die folgende Entwicklung:

printf("%d %d\n", Argument(123), Argument(456));

Das ist sehr interessant. Sicher, es macht keinen Unterschied für diese Ganzzahlargumente, aber ich kann jetzt die Argument-Funktion zum Behandeln von C++-String-Klassen überladen:

template <typename T>
T const * Argument(std::basic_string<T> const & value) noexcept
{
  return value.c_str();
}

Dann kann ich einfach die Druckfunktion mit Streichern aufrufen:

int main()
{
  std::string const hello = "Hello";
  std::wstring const world = L"World";
  Print("%d %s %ls\n", 123, hello, world);
}

Der Compiler wird effektiv die innere Printf-Funktion wie folgt erweitert:

printf("%d %s %ls\n",
  Argument(123), Argument(hello), Argument(world));

Dadurch wird sichergestellt, dass jede Zeichenfolge Null-terminierte Zeichenarray, um Printf bereitgestellt wird und ein ganz klar definiertes Verhalten produziert:

123 Hello World

Zusammen mit der Print Funktionsvorlage verwende ich auch eine Reihe von Überladungen für unformatierte Ausgabe. Dies tendenziell sicherer und verhindert, dass Printf versehentlich falsch beliebige Zeichenketten als "Konvertierungsanweisungen" enthalten. Abbildung 1 sind diese Funktionen aufgeführt.

Abbildung 1 unformatierte Ausgabe drucken

inline void Print(char const * const value) noexcept
{
  Print("%s", value);
}
inline void Print(wchar_t const * const value) noexcept
{
  Print("%ls", value);
}
template <typename T>
void Print(std::basic_string<T> const & value) noexcept
{
  Print(value.c_str());
}

Die ersten beiden Überladungen formatieren bzw. einfach ordentliche und Breitzeichen-Arrays. Die letzte Funktionsvorlage leitet an die entsprechende Überladung, abhängig davon, ob eine Zeichenfolge oder Wstring als Argument angegeben ist. Da diese Funktionen, kann ich sicher einige Konvertierungsanweisungen Drucken buchstäblich, wie folgt:

Print("%d %s %ls\n");

Das sorgt für meine häufigsten Meckerei mit Printf durch Behandeln der Zeichenfolge Ausgang sicher und transparent. Was ist mit Formatierungszeichenfolgen selbst? Der C++-Standardbibliothek bietet verschiedene Varianten von Printf zum Schreiben auf Zeichen Zeichenfolgenpuffer. Davon finde ich Snprintf und Swprintf am wirksamsten. Diese beiden Funktionen behandeln bzw. Charakter und Breitzeichen-Ausgabe. Sie können Sie die maximale Anzahl von Zeichen angeben, die geschrieben werden kann und zurück, ein Wert, der verwendet werden kann, um zu berechnen, wie viel Speicherplatz benötigt wird, sollte das Original Puffer nicht groß genug sein. Sie sind dennoch auf eigene fehleranfällig und ziemlich mühsam zu verwenden. Zeit für einige moderne C++.

Während C überladen nicht unterstützt, ist es weit bequemer, Überlastung in C++ zu verwenden und dies öffnet die Tür für generische Programmierung, so dass ich durch das Einwickeln von Snprintf und Swprintf als StringPrint aufgerufenen Funktionen beginnen werde. Ich verwende auch Variadic Funktionsvorlagen, so dass ich die sichere Argument-Erweiterung nutzen können, die ich zuvor für die Print-Funktion verwendet. Abbildung 2 enthält den Code für beide Funktionen. Diese Funktionen behaupten auch, dass das Ergebnis nicht-1 ist, das ist, was die zugrunde liegenden Funktionen zurück, wenn es einige erzielbare Problem analysieren die Formatzeichenfolge. Ich benutze eine Assertion, weil ich einfach davon ausgehen, das ist ein Bug und vor Versand Produktionscode behoben werden sollte. Möglicherweise möchten Sie ersetzen dies durch eine Ausnahme, aber denken daran, es keine bulletproof Möglichkeit alle Fehler in Ausnahmen zu verwandeln gibt, wie es immer noch möglich ist, ungültige Argumente übergeben, die zu nicht definiertem Verhalten führen. Moderne C++ ist nicht idiotensicher C++.

Abbildung 2 Low-Level Formatierung Zeichenfolgenfunktionen

template <typename ... Args>
int StringPrint(char * const buffer,
                size_t const bufferCount,
                char const * const format,
                Args const & ... args) noexcept
{
  int const result = snprintf(buffer,
                              bufferCount,
                              format,
                              Argument(args) ...);
  ASSERT(-1 != result);
  return result;
}
template <typename ... Args>
int StringPrint(wchar_t * const buffer,
                size_t const bufferCount,
                wchar_t const * const format,
                Args const & ... args) noexcept
{
  int const result = swprintf(buffer,
                              bufferCount,
                              format,
                              Argument(args) ...);
  ASSERT(-1 != result);
  return result;
}

Die StringPrint-Funktionen bieten eine generische Art des Umgangs mit Formatierung von Zeichenfolgen. Jetzt kann ich konzentriere mich auf die Besonderheiten der String-Klasse, und dabei vor allem die Speicherverwaltung. Ich möchte Code wie folgt schreiben:

std::string result;
Format(result, "%d %s %ls", 123, hello, world);
ASSERT("123 Hello World" == result);

Es gibt keine sichtbaren Puffer-Verwaltung. Ich habe nicht herausfinden, wie groß ein Puffer zuordnen. Ich frage einfach die Format-Funktion logischerweise die formatierte Ausgabe das String-Objekt zuweisen. Format sind wie üblich, eine Funktionsvorlage, speziell ein Variadic möglich:

template <typename T, typename ... Args>
void Format(std::basic_string<T> & buffer,
            T const * const format,
            Args const & ... args)
{
}

Es gibt eine Vielzahl von Möglichkeiten, diese Funktion zu implementieren. Einige Experimente und eine gute Portion der Profilerstellung gehen einen langen Weg. Ein einfach aber naiven Ansatz ist anzunehmen, dass die Zeichenfolge entweder leer oder zu klein für die formatierte Ausgabe enthalten ist. In diesem Fall würde ich zunächst Bestimmung der erforderlichen Größe mit StringPrint, Größe den Puffer entsprechend und rufen Sie dann erneut StringPrint mit den richtig zugewiesenen Puffer. Etwa so:

size_t const size = StringPrint(nullptr, 0, format, args ...);
buffer.resize(size);
StringPrint(&buffer[0], buffer.size() + 1, format, args ...);

Die + 1 ist erforderlich, da sowohl die Snprintf als auch die Swprintf übernehmen die gemeldeten Puffergröße enthält einen Bereich für den null-Terminator. Dies funktioniert gut genug, aber es ist offensichtlich, dass ich die Leistung auf dem Tisch verlasse. Eine viel schnellere Methode in den meisten Fällen ist anzunehmen, dass die Zeichenfolge ist groß genug für die formatierte Ausgabe enthalten und bei Bedarf die Größe. Dies fast kehrt den vorhergehenden Code aber ist ganz sicher. Ich möchte zunächst versuchen, die Zeichenfolge direkt in den Puffer zu formatieren:

size_t const size = StringPrint(&buffer[0],
                                buffer.size() + 1,
                                format,
                                args ...);

Wenn die Zeichenfolge ist zu beginnen mit leeren oder einfach nicht groß genug, wird die resultierende Größe größer als die Größe der Zeichenfolge sein und ich werde wissen, ändern Sie die Größe die Zeichenfolge vor dem Aufruf von StringPrint wieder:

if (size > buffer.size())
{
  buffer.resize(size);
  StringPrint(&buffer[0], buffer.size() + 1, format, args ...);
}

Wenn die resultierende Größe kleiner als die Größe der Zeichenfolge ist, werde ich weiß, dass das Format war erfolgreich, aber der Puffer entsprechend gekürzt werden muss:

else if (size < buffer.size())
{
  buffer.resize(size);
}

Schließlich entspricht die Größen gibt es nichts zu tun und die Format-Funktion kann einfach zurück. Die vollständige Vorlage der Format-Funktion finden Sie im Abbildung 3. Wenn Sie mit der String-Klasse vertraut sind, vielleicht Sie erinnern sich, dass es darüber hinaus seine Kapazität meldet und Sie könnte versucht sein, legen Sie die Zeichenfolge Größe entsprechend seiner Kapazität vor dem Aufruf von StringPrint zum ersten Mal denken, dass dies Ihre Gewinnchancen Formatieren der Zeichenfolge richtig zum ersten Mal verbessert werden kann. Die Frage ist, ob ein String-Objekt verkleinert werden kann schneller als Printf die Formatzeichenfolge analysieren und die erforderlichen Puffergröße berechnen kann. Auf der Grundlage meiner informelle Tests, ist die Antwort: kommt drauf an. Sehen Sie, Ändern der Größe einer Zeichenfolge entsprechend seiner Kapazität mehr als nur die gemeldete Größe geändert wird. Alle zusätzlichen Zeichen gelöscht werden müssen, und dies braucht Zeit. Ob dies länger dauert als es Printf nimmt, die Formatzeichenfolge zu analysieren, hängt davon ab, wieviele Zeichen gelöscht werden müssen und wie komplex die Formatierung geschieht, zu sein. Ich benutze einen noch schnelleren Algorithmus für hochwertige­Band ausgeben, aber ich habe festgestellt, dass die Format-Funktion in Abbildung 3 bietet eine gute Leistung für die meisten Szenarien.

Abbildung 3-Formatzeichenfolgen

template <typename T, typename ... Args>
void Format(std::basic_string<T> & buffer,
            T const * const format,
            Args const & ... args)
{
  size_t const size = StringPrint(&buffer[0],
                                  buffer.size() + 1,
                                  format,
                                  args ...);
  if (size > buffer.size())
  {
    buffer.resize(size);
    StringPrint(&buffer[0], buffer.size() + 1, format, args ...);
  }
  else if (size < buffer.size())
  {
    buffer.resize(size);
  }
}

Mit diesem Format-Funktion in der Hand wird es auch sehr einfach, verschiedene Hilfsfunktionen für die Formatierung von Zeichenfolgen Routineoperationen zu schreiben. Vielleicht müssen Sie eine Breitzeichen-Zeichenfolge in eine gewöhnliche String zu konvertieren:

inline std::string ToString(wchar_t const * value)
{
  std::string result;
  Format(result, "%ls", value);
  return result;
}
ASSERT("hello" == ToString(L"hello"));

Vielleicht müssen Sie Gleitkommazahlen zu formatieren:

inline std::string ToString(double const value,
                            unsigned const precision = 6)
{
  std::string result;
  Format(result, "%.*f", precision, value);
  return result;
}
ASSERT("123.46" == ToString(123.456, 2));

Für die Leistung, der besessen ist sind solche spezialisierten Konvertierungsfunktionen auch ganz leicht weiter zu optimieren, da die erforderlichen Puffergrößen etwas vorhersehbar sind, aber ich lasse das auf eigene Faust zu erkunden.

Dies ist nur eine Handvoll nützliche Funktionen aus meiner modernen C++-Ausgabe-Bibliothek. Ich hoffe, dass Ihnen die Inspiration für das moderne C++ verwenden, um einige alte-Schule-C und C++ der Programmiertechniken aktualisieren erteilt habe. Meine Ausgabebibliothek definiert übrigens die Argument-Funktionen sowie die StringPrint-Basisfunktionen in einem geschachtelten internen Namespace. Diese neigt dazu, die Bibliothek zu halten, schön und einfach zu entdecken, aber Sie können Ihre Implementierung anordnen, aber Sie wünschen.


Kenny Kerr ist ein Computerprogrammierer mit Sitz in Kanada, als auch ein Autor für Pluralsight und Microsoft MVP. Er Blogs auf kennykerr.ca und folgen Sie ihm auf Twitter bei twitter.com/kennykerr.