Czy GCC9 unika bezwartościowego stanu std :: variant jest dozwolone?

14

Niedawno śledziłem dyskusję na temat Reddit, która doprowadziła do miłego porównania std::visitoptymalizacji 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_Iz argumentami. std::forward<Args>(args)....Jeśli zostanie zgłoszony wyjątek, *thismoż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_copyablewyklucza 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 emplacenie 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”?

Flamefire
źródło

Odpowiedzi:

7

Myślę, że ważną częścią standardu jest to:

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

23.7.3.4 Modyfikatory

(...)

template variant_alternative_t> & Situace (Args && ... args);

(...) Jeśli podczas inicjowania zawartej wartości zostanie zgłoszony wyjątek, wariant może nie zawierać wartości

Mówi „może” nie „musi”. Spodziewałbym się, że będzie to celowe, aby umożliwić implementacje takie jak ta używana przez gcc.

Jak sam wspomniałeś, jest to możliwe tylko wtedy, gdy niszczyciele wszystkich alternatyw są trywialne, a zatem niemożliwe do zaobserwowania, ponieważ wymagane jest zniszczenie poprzedniej wartości.

Dalsze pytanie:

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

Czy T tmp {std :: forward (args) ...}; this-> value = std :: move (tmp); naprawdę liczą się jako prawidłowe wdrożenie powyższego? Czy to właśnie oznacza „jak gdyby”?

Tak, ponieważ dla typów, które można w prosty sposób skopiować, nie ma możliwości wykrycia różnicy, więc implementacja zachowuje się tak, jakby wartość została zainicjowana zgodnie z opisem. Nie działałoby to, jeśli tego typu nie można w prosty sposób skopiować.

PaulR
źródło
Ciekawy. Zaktualizowałem pytanie prośbą o dalsze działania / wyjaśnienia. Podstawowym jest: Czy kopiowanie / przenoszenie jest dozwolone? Jestem bardzo zdezorientowany tym might/maysformułowaniem, ponieważ standard nie określa, jaka jest alternatywa.
Flamefire
Akceptując to dla standardowej oferty i there is no way to detect the difference.
Flamefire,
5

Czy zatem standard rzeczywiście dopuszcza, że emplacenie zmienia to bieżącej wartości?

Tak. emplacezapewnia 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).

variantmusi 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 typu emplacenie 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.

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”?

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ć.

LF
źródło
W pełni zgadzam się z logicznym rozumowaniem. Po prostu nie jestem pewien, czy tak naprawdę jest w standardzie? Czy możesz coś z tym zrobić?
Flamefire,
@Flamefire Hmm ... Ogólnie rzecz biorąc, standardowe funkcje zapewniają podstawową gwarancję (chyba że coś jest nie tak z tym, co zapewnia użytkownik) i std::variantnie 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ą.
LF,
Dzięki. Wewnątrz znajduje się bardziej wyraźna specyfikacja: if an exception is thrown during the call toT’s constructor, valid()will be false;więc to zabroniło tej „optymalizacji”
Flamefire,
Tak. Specyfikacja emplacew P0088 underException safety
Flamefire
@Flamefire Wydaje się, że istnieje rozbieżność między pierwotną propozycją a wersją, w której głosowano. Ostateczna wersja zmieniona na sformułowanie „może”.
LF