Ist es GCC9 erlaubt, den wertlosen Zustand der std :: -Variante zu vermeiden?

14

Ich habe kürzlich eine Reddit-Diskussion verfolgt, die zu einem schönen Vergleich der std::visitOptimierung zwischen Compilern führte. Mir ist Folgendes aufgefallen: https://godbolt.org/z/D2Q5ED

Sowohl GCC9 als auch Clang9 (ich denke, sie haben dieselbe stdlib) generieren keinen Code zum Überprüfen und Auslösen einer wertlosen Ausnahme, wenn alle Typen bestimmte Bedingungen erfüllen. Dies führt zu einem viel besseren Codegen, daher habe ich ein Problem mit der MSVC STL angesprochen und wurde mit diesem Code konfrontiert:

template <class T>
struct valueless_hack {
  struct tag {};
  operator T() const { throw tag{}; }
};

template<class First, class... Rest>
void make_valueless(std::variant<First, Rest...>& v) {
  try { v.emplace<0>(valueless_hack<First>()); }
  catch(typename valueless_hack<First>::tag const&) {}
}

Die Behauptung war, dass dies jede Variante wertlos macht und das Dokument lesen sollte:

Zerstört zunächst den aktuell enthaltenen Wert (falls vorhanden). Initialisiert dann den enthaltenen Wert direkt, als würde ein Wert vom Typ T_Imit den Argumenten erstellt. std::forward<Args>(args)....Wenn eine Ausnahme ausgelöst wird, *thiskann dies zu einer wertlosen Ausnahme führen .

Was ich nicht verstehe: Warum wird es als "Mai" angegeben? Ist es legal, im alten Zustand zu bleiben, wenn die ganze Operation auslöst? Denn genau das macht GCC:

  // For suitably-small, trivially copyable types we can create temporaries
  // on the stack and then memcpy them into place.
  template<typename _Tp>
    struct _Never_valueless_alt
    : __and_<bool_constant<sizeof(_Tp) <= 256>, is_trivially_copyable<_Tp>>
    { };

Und später macht es (bedingt) so etwas wie:

T tmp  = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);

Daher wird im Grunde genommen ein temporäres Objekt erstellt, und wenn dies erfolgreich ist, wird es kopiert / an den realen Ort verschoben.

IMO ist dies eine Verletzung von "Zerstört zuerst den aktuell enthaltenen Wert", wie im Dokument angegeben. Wenn ich den Standard lese, wird nach a v.emplace(...)der aktuelle Wert in der Variante immer zerstört und der neue Typ ist entweder der eingestellte Typ oder wertlos.

Ich verstehe, dass die Bedingung is_trivially_copyablealle Typen ausschließt, die einen beobachtbaren Destruktor haben. Dies kann aber auch so lauten: "Als ob die Variante mit dem alten Wert neu initialisiert wird" oder so. Der Zustand der Variante ist jedoch ein beobachtbarer Effekt. Erlaubt der Standard also tatsächlich, dass emplacesich der aktuelle Wert nicht ändert?

Bearbeiten als Antwort auf ein Standardangebot:

Initialisiert dann den enthaltenen Wert so, als würde ein Wert vom Typ TI mit den Argumenten direkt ohne Listeninitialisierung initialisiert std​::​forward<Args>(args)....

Gilt das T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);wirklich als gültige Implementierung des oben genannten? Ist das das, was mit "als ob" gemeint ist?

Flammenfeuer
quelle

Antworten:

7

Ich denke, der wichtige Teil des Standards ist folgender:

Von https://timsong-cpp.github.io/cppwp/n4659/variant.mod#12

23.7.3.4 Modifikatoren

(...)

template varian_alternative_t> & emplace (Args && ... args);

(...) Wenn während der Initialisierung des enthaltenen Werts eine Ausnahme ausgelöst wird, enthält die Variante möglicherweise keinen Wert

Es heißt "könnte" nicht "muss". Ich würde erwarten, dass dies beabsichtigt ist, um Implementierungen wie die von gcc verwendete zu ermöglichen.

Wie Sie selbst erwähnt haben, ist dies nur möglich, wenn die Destruktoren aller Alternativen trivial und daher nicht beobachtbar sind, da die Zerstörung des vorherigen Werts erforderlich ist.

Zusatzfrage:

Then initializes the contained value as if direct-non-list-initializing a value of type TI with the arguments std​::​forward<Args>(args)....

Tut T tmp {std :: forward (args) ...}; this-> value = std :: move (tmp); wirklich als gültige Implementierung der oben genannten zählen? Ist das das, was mit "als ob" gemeint ist?

Ja, da es für Typen, die trivial kopierbar sind, keine Möglichkeit gibt, den Unterschied zu erkennen. Die Implementierung verhält sich also so, als ob der Wert wie beschrieben initialisiert wurde. Dies würde nicht funktionieren, wenn der Typ nicht trivial kopierbar wäre.

PaulR
quelle
Interessant. Ich habe die Frage mit einer Folge- / Klärungsanfrage aktualisiert. Die Wurzel ist: Ist das Kopieren / Verschieben erlaubt? Der might/mayWortlaut verwirrt mich sehr, da der Standard nicht angibt, was die Alternative ist.
Flamefire
Akzeptieren Sie dies für das Standardangebot und there is no way to detect the difference.
Flammenfeuer
5

Erlaubt der Standard also tatsächlich, dass emplacesich der aktuelle Wert nicht ändert?

Ja. emplacemuss die grundlegende Garantie dafür geben, dass keine Undichtigkeiten auftreten (dh die Lebensdauer des Objekts wird beachtet, wenn Bau und Zerstörung beobachtbare Nebenwirkungen hervorrufen), aber wenn möglich, darf die starke Garantie gegeben werden (dh der ursprüngliche Zustand bleibt erhalten, wenn ein Vorgang fehlschlägt).

variantmuss sich ähnlich wie eine Gewerkschaft verhalten - die Alternativen werden in einer Region mit entsprechend zugewiesenem Speicher zugewiesen. Es ist nicht zulässig, dynamischen Speicher zuzuweisen. Daher ein Typwechselemplace Typänderung das ursprüngliche Objekt nicht beibehalten, ohne einen zusätzlichen Verschiebungskonstruktor aufzurufen. Sie muss es zerstören und das neue Objekt anstelle des Konstrukts erstellen. Wenn diese Konstruktion fehlschlägt, muss die Variante in den außergewöhnlichen wertlosen Zustand versetzt werden. Dies verhindert seltsame Dinge wie die Zerstörung eines nicht vorhandenen Objekts.

Bei kleinen, trivial kopierbaren Typen ist es jedoch möglich, die starke Garantie ohne zu viel Overhead bereitzustellen (in diesem Fall sogar eine Leistungssteigerung, um eine Überprüfung zu vermeiden). Daher macht es die Implementierung. Dies ist standardkonform: Die Implementierung bietet weiterhin die vom Standard geforderte Grundgarantie, nur auf benutzerfreundlichere Weise.

Bearbeiten als Antwort auf ein Standardangebot:

Initialisiert dann den enthaltenen Wert so, als würde ein Wert vom Typ TI mit den Argumenten direkt ohne Listeninitialisierung initialisiert std​::​forward<Args>(args)....

Gilt das T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);wirklich als gültige Implementierung des oben genannten? Ist das das, was mit "als ob" gemeint ist?

Ja, wenn die Verschiebungszuweisung keinen beobachtbaren Effekt erzeugt, was bei trivial kopierbaren Typen der Fall ist.

LF
quelle
Ich stimme der logischen Argumentation voll und ganz zu. Ich bin mir nur nicht sicher, ob dies tatsächlich im Standard enthalten ist. Können Sie dies mit irgendetwas belegen?
Flamefire
@Flamefire Hmm ... Im Allgemeinen bieten die Standardfunktionen die grundlegende Garantie (es sei denn, mit dem, was der Benutzer bereitstellt, stimmt etwas nicht), und es std::variantgibt keinen Grund, dies zu brechen. Ich bin damit einverstanden, dass dies im Wortlaut des Standards deutlicher herausgestellt werden kann, aber im Grunde funktionieren andere Teile der Standardbibliothek so. Und zu Ihrer Information , P0088 war der ursprüngliche Vorschlag.
LF
Vielen Dank. Es gibt eine explizitere Spezifikation im Inneren: if an exception is thrown during the call toT’s constructor, valid()will be false;Das hat diese "Optimierung" verboten
Flamefire
Ja. Spezifikation von emplacein P0088 unterException safety
Flamefire
@Flamefire Scheint eine Diskrepanz zwischen dem ursprünglichen Vorschlag und der Version zu sein, in der abgestimmt wurde. Die endgültige Version wurde in den Wortlaut "Mai" geändert.
LF