Formatierungsklasse

1755
John Dibling

In unserem Produktionscode können wir Boost oder C ++ 0x nicht verwenden. Das Formatieren von Strings mit sprintfoder stringstreamist in diesem Fall ärgerlich, und das hat mich dazu veranlasst, meine eigene kleine FormatterKlasse zu schreiben . Ich bin neugierig, ob die Implementierung dieser Klasse oder deren Verwendung ein undefiniertes Verhalten mit sich bringt.

Ist diese Zeile insbesondere definiert:

Reject( Formatter() << "Error Recieved" << 42 << " " << some_code << " '" << some_msg << "'");

Mein Glaube ist, dass es OK ist, aber ich wollte Peer-Review.

Drei Hauptprobleme:

  1. Gibt es eine Doppelzuordnung innerhalb eines einzelnen Sequenzpunktes? Ist es UB?
  2. Habe ich ein Problem mit der Lebensdauer von Provisorien?
  3. Führt meine FormatterKlasse (oder der beabsichtigte Gebrauch davon) eine UB ein?

Die FormatterKlasse hat sowohl eine (Template) operator<<als auch eine operator std::string. Die Absicht ist es, die Formatter()Klasse als temporäre Stelle anstelle eines std::stringParameters für jede Funktion zu verwenden, die ein const std::string&.

Hier ist die Klassendefinition:

class Formatter
{
public:
 Formatter() {};
 template<class Field> Formatter& operator<<(Field f)
 {
  ss_ << f;
  return *this;
 }
 operator std::string() const { return ss_.str(); }
private:
 std::stringstream ss_;
};

Und hier ist ein kompletter Testgurt, einschließlich der obigen Definition. Sie sollten in der Lage sein, so zu kompilieren und auszuführen, wie sie sind. Sehen Sie eine UB?

#include <cstdlib>
#include <string>
#include <sstream>
#include <iostream>

class Formatter
{
public:
 Formatter() {};
 template<class Field> Formatter& operator<<(Field f)
 {
  ss_ << f;
  return *this;
 }
 operator std::string() const { return ss_.str(); }
private:
 std::stringstream ss_;
};

void Reject(const std::string& msg)
{
 std::cout << "Recieved Message: '" << msg << "'" << std::endl;
}

int main()
{
 const char& some_code = 'A';
 const char* some_msg = "Something";

 Reject( Formatter() << "Error Recieved" << 42 << " " << some_code << " '" << some_msg << "'");
}
Antworten
25

6 Antworten auf die Frage

9
Fred Nurk

Zusätzlich zu dem, was bereits gesagt wurde, würde ich:

  • Markiere den Stringstream als public. Dies wirkt sich nicht auf die meisten Verwendungen Ihres Codes aus und kann bereits mit einem benutzerdefinierten Manipulator gehackt werden, um auf das "interne" Stream-Objekt zuzugreifen. Dies ermöglicht jedoch den Zugriff auf den internen Stream (z. B. um die Zeichenfolge zu vermeiden) Kopie, die der Stringstream-Schnittstelle innewohnt), und kennen die Einzelheiten ihrer Implementierung, die das ermöglichen, was sie wollen. Die 0x-Move-Semantik beseitigt natürlich dieses Bedürfnis, ist aber immer noch nicht ganz hier.

  • Überprüfen Sie den Stream, bevor Sie die Zeichenfolge zurückgeben. Wenn der Status fehlgeschlagen ist, lösen Sie eine Ausnahme aus (oder protokollieren Sie die Bedingung mindestens, bevor Sie eine Zeichenfolge zurückgeben). Bei den meisten Anwendungen ist dies unwahrscheinlich, aber wenn dies der Fall ist, werden Sie froh sein, dass Sie herausgefunden haben, dass der Stream ausgefallen ist, anstatt sich mit der Formatierung zu beschäftigen, während Sie sich fragen, warum "es nicht funktioniert".

Bezüglich der Doppelzuweisung gibt es überhaupt keine Zuordnung. Die Sequenzpunkte sollten größtenteils so sein, wie die Leute es erwarten, aber genau es sieht so aus:

some_function(((Formatter() << expr_a) << expr_b) << expr_c);
//                          1          2          3

Die Bediener ordnen es so an, als wären es Funktionsaufrufe, so dass:

  • Formatter () und expr_a treten beide vor dem Einfügezeichen 1 auf.
  • Die obigen Angaben plus Einfügung 1 und expr_b werden vor der Einfügung 2 ausgeführt.
  • Das Vorstehende plus Einfügung 2 plus expr_c geschieht vor dem Einfügen 3.
  • Beachten Sie, dass dies nur in einer Richtung Grenzen setzt: expr_c kann beispielsweise nach expr_a und vor Formatter () auftreten.
  • Vor dem Aufruf von some_function werden natürlich alle oben genannten Punkte sowie die Konvertierung von Strings ausgeführt.

Um die Diskussion über Temporäre zu ergänzen, sind alle erstellten Temporaries im Ausdruck:

some_function(Formatter() << make_a_temp() << "etc.")
//            one temp       another temp     and so on

Sie werden erst am Ende des vollständigen Ausdrucks zerstört, der den Aufruf some_function enthält. Dies bedeutet, dass die Zeichenfolge nicht nur an some_function übergeben wird, sondern dass some_function bis zu diesem Zeitpunkt bereits zurückgegeben wurde. (Oder es wird eine Ausnahme ausgelöst und beim Abwickeln zerstört usw.)

Um alle Manipulatoren wie std :: endl zu behandeln, fügen Sie Folgendes hinzu :

struct Formatter {
  Formatter& operator<<(std::ios_base& (*manip)(std::ios_base&)) {
    ss_ << manip;
    return *this;
  }
  Formatter& operator<<(std::ios& (*manip)(std::ios&)) {
    ss_ << manip;
    return *this;
  }
  Formatter& operator<<(std::ostream& (*manip)(std::ostream&)) {
    ss_ << manip;
    return *this;
  }
};

Ich habe dieses Muster mehrmals verwendet, um Streams zu umschließen, und es ist sehr praktisch, da Sie es (wie Sie) inline verwenden oder eine Formatter-Variable für komplexere Manipulationen erstellen können (denken Sie an eine Schleife, die in den Stream eingefügt wird, basierend auf einer Bedingung. usw.). Der letztere Fall ist jedoch nur wichtig, wenn der Wrapper mehr tut, als Sie es hier tun. :)

Die Gefahr ist, dass "expr_c" zur gleichen Zeit wie "expr_a" passieren kann. Deduplicator vor 5 Jahren 0
6
Martin York

Nur drei Kommentare:

  1. Ich würde den leeren Konstruktor entfernen.

  2. Was ist mit dem Umgang mit std :: manipulators?

  3. Möchten Sie die Feldwerte nicht als konstante Referenz übergeben?

.

 template<class Field> Formatter& operator<<(Field const& f)
                                                // ^^^^^^

Ihre Anliegen:

  1. Gibt es eine Doppelzuordnung innerhalb eines einzelnen Sequenzpunktes? Ist es UB?

Sehe keinen
Sieht gut aus.

  1. Habe ich ein Problem mit der Lebensdauer von Provisorien?

Glaube nicht so.

Was ist das Problem mit Manipulatoren? Sie können immer noch den Operator für das Einfügen von Vorlagen verwenden. `Formatter () << std :: hex;`. Meinen Sie damit Sachen wie `Formatter (). Width (10)`? wilhelmtell vor 10 Jahren 0
@wilhelmtell: Eigentlich habe ich über std :: endl nachgedacht. Aber das kann mit Absicht sein. Obwohl die Verwendung von std :: endl im Kontext mit dem Formatter-Objekt möglicherweise keine große Bedeutung hat, kann es dennoch einige Benutzer verwirren, die einfach alten Code portieren, den dieses Streamable-Objekt nicht kompilieren kann, wenn es mit std :: endl verwendet wird. Martin York vor 10 Jahren 0
Manipulatoren funktionieren nicht direkt. Sie müssen die Unterstützung manuell hinzufügen. Schauen Sie sich die [Antwort] (http://codereview.stackexchange.com/questions/226/does-my-class-introduce-undefined-behavior/263#263) von Fred Nurk an David Rodríguez - dribeas vor 10 Jahren 0
@ David Rodríguez - dribeas: Genau deswegen habe ich es erwähnt. Martin York vor 10 Jahren 0
York, ich hätte dort @wilhelmtell hinzufügen sollen. In dem Kommentar ging es nicht um Ihre Antwort, sondern um seinen Kommentar. Entschuldigung für die Verwirrung :) David Rodríguez - dribeas vor 10 Jahren 0
5
MSN

Ich denke, Sie machen sich vielleicht Sorgen über zeitweilige Probleme. (Ich weiß, dass ich auf ähnlichem Code basiert.)

Das als Parameter erstellte temporäre Objekt Rejecthat eine Lebensdauer, die an den Ausdruck gebunden ist, in dem es erstellt wird. (Irgendwo im C ++ - Standard.) Selbst wenn Ihre Konvertierungsoperatoren Werte zurückgeben, werden sie alle nach dem Ausdruck, der enthält, zerstört Reject.

+1: Ich war auch besorgt über die Doppelzuweisung innerhalb eines einzelnen Sequenzpunktes. John Dibling vor 10 Jahren 0
5
wilhelmtell
  • Ich würde a verwenden, std::ostringstreamweil Sie die Formatierungsfunktionen von nicht verwenden (scheinen) std::istringstream.
  • Was passiert, wenn die Formatierung fehlschlägt? Können Sie nach Fehlern suchen?
4
Mark Loeser

Sieht gut aus. Machen Sie sich nicht zu viele Sorgen und überarbeiten Sie eine einfache Lösung :)

Bearbeiten:

Eigentlich würde ich die Parameter operator<<in const-Referenzen umwandeln.

Genau! Ich hatte gehofft, dass es kein Problem gab, denn "Formatter" ist in der Tat ein sehr kleines Gizmo. John Dibling vor 10 Jahren 0
4
cdmh

Sieht ok aus. Der leere Ctor ist nicht erforderlich. Der vom Compiler generierte wird gut funktionieren. "Empfangen" sollte "Empfangen" sein :)