È consentito GCC9 evitando lo stato senza valore di std :: variant?


14

Di recente ho seguito una discussione su Reddit che ha portato a un bel confronto di std::visitottimizzazione tra i compilatori. Ho notato quanto segue: https://godbolt.org/z/D2Q5ED

Sia GCC9 che Clang9 (immagino condividano lo stesso stdlib) non generano codice per controllare e generare un'eccezione senza valore quando tutti i tipi soddisfano alcune condizioni. Questo porta a una migliore codegen, quindi ho sollevato un problema con MSVC STL e mi è stato presentato questo codice:

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&) {}
}

L'affermazione era che ciò rende qualsiasi variante senza valore e, leggendo il documento , dovrebbe:

Innanzitutto, distrugge il valore attualmente contenuto (se presente). Quindi inizializza direttamente il valore contenuto come se costruisse un valore di tipo T_Icon gli argomenti std::forward<Args>(args)....Se viene generata un'eccezione, *thispuò diventare valueeless_by_exception.

Quello che non capisco: perché è indicato come "può"? È legale rimanere nel vecchio stato se l'intera operazione viene lanciata? Perché questo è ciò che fa 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>>
    { };

E più tardi (condizionatamente) fa qualcosa di simile:

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

Quindi fondamentalmente crea un temporaneo, e se ciò riesce riesce a copiarlo / spostarlo nel luogo reale.

IMO questa è una violazione di "In primo luogo, distrugge il valore attualmente contenuto" come affermato dal docu. Mentre leggo lo standard, dopo un v.emplace(...)valore corrente nella variante viene sempre distrutto e il nuovo tipo è il tipo impostato o senza valore.

Capisco che la condizione is_trivially_copyableesclude tutti i tipi che hanno un distruttore osservabile. Quindi questo può anche essere come: "come se la variante è reinizializzata con il vecchio valore" o giù di lì. Ma lo stato della variante è un effetto osservabile. Quindi lo standard permette davvero, che emplacenon cambia il valore corrente?

Modifica in risposta a un preventivo standard:

Quindi inizializza il valore contenuto come se l'inizializzazione diretta-non-elenco di un valore di tipo TI con gli argomenti std​::​forward<Args>(args)....

Fa T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);davvero conta come una valida attuazione di quanto sopra? È questo che si intende per "come se"?

Risposte:


7

Penso che la parte importante dello standard sia questa:

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

23.7.3.4 Modificatori

(...)

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

(...) Se viene generata un'eccezione durante l'inizializzazione del valore contenuto, la variante potrebbe non contenere un valore

Dice "potrebbe" non "dovere". Mi aspetto che ciò sia intenzionale per consentire implementazioni come quella usata da gcc.

Come hai detto te stesso, questo è possibile solo se i distruttori di tutte le alternative sono banali e quindi non osservabili perché è necessario distruggere il valore precedente.

Domanda di follow-up:

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

T tmp {std :: forward (args) ...}; this-> value = std :: move (tmp); conta davvero come una valida implementazione di quanto sopra? È questo che si intende per "come se"?

Sì, perché per tipi che sono banalmente copiabili non c'è modo di rilevare la differenza, quindi l'implementazione si comporta come se il valore fosse inizializzato come descritto. Ciò non funzionerebbe se il tipo non fosse banalmente copiabile.


Interessante. Ho aggiornato la domanda con una richiesta di follow-up / chiarimenti. Il root è: è consentita la copia / spostamento? Sono molto confuso dalla might/mayformulazione in quanto lo standard non indica quale sia l'alternativa.
Fiamma

Accettando questo per il preventivo standard e there is no way to detect the difference.
Fiamma

5

Quindi lo standard permette davvero, che emplacenon cambia il valore corrente?

Sì. emplacedeve fornire la garanzia di base di assenza di perdite (vale a dire, rispetto della durata dell'oggetto quando la costruzione e la distruzione producono effetti collaterali osservabili), ma quando possibile, è consentito fornire la garanzia forte (ovvero, lo stato originale viene mantenuto quando un'operazione fallisce).

variantè tenuto a comportarsi in modo analogo a un sindacato: le alternative sono allocate in una regione di archiviazione adeguatamente allocata. Non è consentito allocare memoria dinamica. Pertanto, un cambio di tipo emplacenon ha modo di mantenere l'oggetto originale senza chiamare un costruttore di mosse aggiuntivo: deve distruggerlo e costruire il nuovo oggetto al posto di esso. Se questa costruzione fallisce, allora la variante deve passare allo stato eccezionale senza valore. Questo impedisce cose strane come la distruzione di un oggetto inesistente.

Tuttavia, per piccoli tipi banalmente copiabili, è possibile fornire la forte garanzia senza troppe spese generali (anche un aumento delle prestazioni per evitare un controllo, in questo caso). Pertanto, l'implementazione lo fa. Questo è conforme allo standard: l'implementazione fornisce ancora la garanzia di base come richiesto dallo standard, solo in un modo più user-friendly.

Modifica in risposta a un preventivo standard:

Quindi inizializza il valore contenuto come se l'inizializzazione diretta-non-elenco di un valore di tipo TI con gli argomenti std​::​forward<Args>(args)....

Fa T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);davvero conta come una valida attuazione di quanto sopra? È questo che si intende per "come se"?

Sì, se l'assegnazione di spostamento non produce alcun effetto osservabile, come nel caso di tipi banalmente copiabili.


Sono pienamente d'accordo con il ragionamento logico. Non sono sicuro che questo sia effettivamente nello standard? Puoi supportarlo con qualcosa?
Fiamma

@Flamefire Hmm ... In generale, le funzionalità standard forniscono la garanzia di base (a meno che non ci sia qualcosa di sbagliato in ciò che l'utente fornisce) e std::variantnon ha motivo di romperlo. Concordo sul fatto che ciò può essere reso più esplicito nella formulazione dello standard, ma questo è fondamentalmente il modo in cui funzionano gli altri componenti della libreria standard. E Cordiali saluti, P0088 era la proposta iniziale.
LF

Grazie. C'è una specifica più esplicita all'interno: if an exception is thrown during the call toT’s constructor, valid()will be false;Quindi ciò ha proibito questa "ottimizzazione"
Flamefire,

Sì. Specifica di emplacein P0088 sottoException safety
Fiamma

@Flamefire Sembra essere una discrepanza tra la proposta originale e la versione votata. La versione finale è cambiata nella formulazione "maggio".
LF
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.