Ich habe kürzlich eine Reddit-Diskussion verfolgt, die zu einem schönen Vergleich der std::visit
Optimierung 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_I
mit den Argumenten erstellt.std::forward<Args>(args)....
Wenn eine Ausnahme ausgelöst wird,*this
kann 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_copyable
alle 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 emplace
sich 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?
quelle
might/may
Wortlaut verwirrt mich sehr, da der Standard nicht angibt, was die Alternative ist.there is no way to detect the difference
.Ja.
emplace
muss 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).variant
muss 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.
Ja, wenn die Verschiebungszuweisung keinen beobachtbaren Effekt erzeugt, was bei trivial kopierbaren Typen der Fall ist.
quelle
std::variant
gibt 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.if an exception is thrown during the call toT’s constructor, valid()will be false;
Das hat diese "Optimierung" verbotenemplace
in P0088 unterException safety