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”?
might/may
sformułowaniem, ponieważ standard nie określa, jaka jest alternatywa.