Niedawno śledziłem dyskusję na temat Reddit, która doprowadziła do miłego porównania std::visit
optymalizacji między kompilatorami. Zauważyłem, co następuje: https://godbolt.org/z/D2Q5ED
Zarówno GCC9, jak i Clang9 (myślę, że współużytkują ten sam stdlib) nie generują kodu do sprawdzania i zgłaszania wyjątku bezwartościowego, gdy wszystkie typy spełniają określone warunki. To prowadzi do znacznie lepszego codegen, dlatego podniosłem problem z MSVC STL i został przedstawiony z tym kodem:
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&) {}
}
Twierdzenie było takie, że powoduje to, że każdy wariant jest bezwartościowy, a czytanie dokumentu powinno:
Po pierwsze, niszczy aktualnie zawartą wartość (jeśli istnieje). Następnie bezpośrednio inicjuje zawartą wartość tak, jakby konstruowano wartość typu
T_I
z argumentami.std::forward<Args>(args)....
Jeśli zostanie zgłoszony wyjątek,*this
może stać się wyjątkiem bezwartościowym.
Czego nie rozumiem: Dlaczego podano to jako „może”? Czy legalne jest pozostanie w starym stanie, jeśli cała operacja zostanie rzucona? Ponieważ to właśnie robi 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>>
{ };
A później (warunkowo) robi coś takiego:
T tmp = forward(args...);
reset();
construct(tmp);
// Or
variant tmp(inplace_index<I>, forward(args...));
*this = move(tmp);
Stąd w zasadzie tworzy tymczasowy, a jeśli to się powiedzie, kopiuje / przenosi go w prawdziwe miejsce.
IMO jest to naruszenie „Po pierwsze, niszczy aktualnie zawartą wartość”, jak stwierdzono w dokumencie. Jak czytam standard, to po v.emplace(...)
bieżącej wartości w wariancie jest zawsze niszczony, a nowy typ jest albo ustawionym, albo bezwartościowym.
Rozumiem, że warunek is_trivially_copyable
wyklucza wszystkie typy, które mają obserwowalny destruktor. Można to również traktować jako: „wariant tak jak gdyby został ponownie zainicjowany ze starą wartością” lub mniej więcej. Ale stan wariantu jest zauważalnym efektem. Czy zatem standard rzeczywiście dopuszcza, że emplace
nie zmienia to bieżącej wartości?
Edytuj w odpowiedzi na standardową ofertę:
Następnie inicjuje zawartą wartość tak, jakby inicjalizowała bezpośrednio listę bez inicjowania wartości typu TI z argumentami
std::forward<Args>(args)...
.
Czy T tmp {std::forward<Args>(args)...}; this->value = std::move(tmp);
tak naprawdę jest to ważna implementacja powyższego? Czy to właśnie oznacza „jak gdyby”?
źródło
might/may
sformułowaniem, ponieważ standard nie określa, jaka jest alternatywa.there is no way to detect the difference
.Tak.
emplace
zapewnia podstawową gwarancję braku wycieków (tj. poszanowanie żywotności obiektu, gdy konstrukcja i zniszczenie powodują obserwowalne skutki uboczne), ale w miarę możliwości można udzielić silnej gwarancji (tj. zachowanie pierwotnego stanu w przypadku niepowodzenia operacji).variant
musi zachowywać się podobnie jak związek - alternatywy są przydzielane w jednym regionie odpowiednio przydzielonego miejsca do magazynowania. Nie wolno przydzielać pamięci dynamicznej. Dlatego zmiana typuemplace
nie ma sposobu na zachowanie oryginalnego obiektu bez wywołania dodatkowego konstruktora ruchu - musi go zniszczyć i zbudować nowy obiekt zamiast niego. Jeśli ta konstrukcja zawiedzie, wariant musi przejść w wyjątkowy stan bezwartościowy. Zapobiega to dziwnym rzeczom, takim jak niszczenie nieistniejącego obiektu.Jednak w przypadku małych, trywialnych typów możliwe jest zapewnienie silnej gwarancji bez nadmiernego obciążenia (w tym przypadku nawet zwiększenie wydajności w celu uniknięcia sprawdzenia). Dlatego implementacja to robi. Jest to zgodne ze standardami: wdrożenie nadal zapewnia podstawową gwarancję wymaganą przez standard, tylko w sposób bardziej przyjazny dla użytkownika.
Tak, jeśli przypisanie ruchu nie daje żadnego możliwego do zaobserwowania efektu, co ma miejsce w przypadku typów, które można łatwo skopiować.
źródło
std::variant
nie ma powodu, aby to łamać. Zgadzam się, że można to wyrazić bardziej precyzyjnie w brzmieniu standardu, ale w zasadzie tak działają inne elementy standardowej biblioteki. I do Twojej wiadomości, P0088 była wstępną propozycją.if an exception is thrown during the call toT’s constructor, valid()will be false;
więc to zabroniło tej „optymalizacji”emplace
w P0088 underException safety