GCC9 évite-t-il l'état sans valeur de std :: variant autorisé?

14

J'ai récemment suivi une discussion sur Reddit qui a conduit à une belle comparaison de l' std::visitoptimisation entre les compilateurs. J'ai remarqué ce qui suit: https://godbolt.org/z/D2Q5ED

GCC9 et Clang9 (je suppose qu'ils partagent le même stdlib) ne génèrent pas de code pour vérifier et lever une exception sans valeur lorsque tous les types remplissent certaines conditions. Cela conduit à un meilleur codegen, donc j'ai soulevé un problème avec le MSVC STL et j'ai été présenté avec ce code:

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'affirmation était que cela rend toute variante sans valeur, et la lecture du docu devrait:

Tout d'abord, détruit la valeur actuellement contenue (le cas échéant). Puis initialise directement la valeur contenue comme si la construction d'une valeur de type T_Iavec les arguments std::forward<Args>(args)....Si une exception est levée, *thispeut devenir sans valeur_par_exception.

Ce que je ne comprends pas: pourquoi est-il dit "peut"? Est-il légal de rester dans l'ancien état si toute l'opération se termine? Parce que c'est ce que fait 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>>
    { };

Et plus tard, il fait (conditionnellement) quelque chose comme:

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

Par conséquent, fondamentalement, il crée un temporaire, et si cela réussit, le copie / le déplace à la place réelle.

OMI, il s'agit d'une violation de "Tout d'abord, détruit la valeur actuellement contenue" comme indiqué par le docu. Comme je l'ai lu la norme, puis après un v.emplace(...)la valeur actuelle dans la variante est toujours détruite et le nouveau type est soit le type défini ou sans valeur.

Je comprends que la condition is_trivially_copyableexclut tous les types qui ont un destructeur observable. Donc, cela peut aussi être bien comme: "comme si la variante est réinitialisée avec l'ancienne valeur" ou plus. Mais l'état de la variante est un effet observable. La norme le permet-elle en effet, cela emplacene change pas la valeur actuelle?

Modifier en réponse à un devis standard:

Initialise ensuite la valeur contenue comme si l'initialisation directe sans liste d'une valeur de type TI avec les arguments std​::​forward<Args>(args)....

Est-ce que T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);cela compte vraiment comme une implémentation valide de ce qui précède? Est-ce ce que l'on entend par «comme si»?

Flamefire
la source

Réponses:

7

Je pense que la partie importante de la norme est la suivante:

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

23.7.3.4 Modi fi cateurs

(...)

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

(...) Si une exception est levée lors de l'initialisation de la valeur contenue, la variante peut ne pas contenir de valeur

Il dit "pourrait" pas "doit". Je m'attendrais à ce que ce soit intentionnel afin de permettre des implémentations comme celle utilisée par gcc.

Comme vous l'avez mentionné vous-même, cela n'est possible que si les destructeurs de toutes les alternatives sont triviaux et donc inobservables car la destruction de la valeur précédente est nécessaire.

Question de suivi:

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

Est-ce que T tmp {std :: forward (args) ...}; this-> value = std :: move (tmp); compte vraiment comme une implémentation valide de ce qui précède? Est-ce ce que l'on entend par «comme si»?

Oui, car pour les types qui sont trivialement copiables, il n'y a aucun moyen de détecter la différence, donc l'implémentation se comporte comme si la valeur était initialisée comme décrit. Cela ne fonctionnerait pas si le type n'était pas trivialement copiable.

PaulR
la source
Intéressant. J'ai mis à jour la question avec une demande de suivi / clarification. La racine est: la copie / le déplacement est-il autorisé? Je suis très confus par le might/maylibellé, car la norme ne précise pas quelle est l'alternative.
Flamefire
L'accepter pour le devis standard et there is no way to detect the difference.
Flamefire
5

La norme le permet-elle en effet, cela emplacene change pas la valeur actuelle?

Oui. emplacedoit fournir la garantie de base contre les fuites (c'est-à-dire respecter la durée de vie des objets lorsque la construction et la destruction produisent des effets secondaires observables), mais lorsque cela est possible, il est autorisé à fournir une garantie solide (c'est-à-dire que l'état d'origine est conservé en cas d'échec d'une opération).

variantdoit se comporter de manière similaire à une union - les alternatives sont allouées dans une région de stockage correctement allouée. Il n'est pas autorisé d'allouer de la mémoire dynamique. Par conséquent, un changement de type emplacen'a aucun moyen de conserver l'objet d'origine sans appeler un constructeur de déplacement supplémentaire - il doit le détruire et construire le nouvel objet à sa place. Si cette construction échoue, alors la variante doit passer à l'état exceptionnel sans valeur. Cela empêche des choses étranges comme la destruction d'un objet inexistant.

Cependant, pour les petits types trivialement copiables, il est possible de fournir la garantie forte sans trop de surcharge (même un gain de performances pour éviter un contrôle, dans ce cas). Par conséquent, l'implémentation le fait. Ceci est conforme à la norme: l'implémentation fournit toujours la garantie de base requise par la norme, juste d'une manière plus conviviale.

Modifier en réponse à un devis standard:

Initialise ensuite la valeur contenue comme si l'initialisation directe sans liste d'une valeur de type TI avec les arguments std​::​forward<Args>(args)....

Est-ce que T tmp {std​::​forward<Args>(args)...}; this->value = std::move(tmp);cela compte vraiment comme une implémentation valide de ce qui précède? Est-ce ce que l'on entend par «comme si»?

Oui, si l'affectation de déplacement ne produit aucun effet observable, ce qui est le cas pour les types trivialement copiables.

LF
la source
Je suis entièrement d'accord avec le raisonnement logique. Je ne suis juste pas sûr que ce soit dans la norme? Pouvez-vous sauvegarder cela avec quelque chose?
Flamefire
@Flamefire Hmm ... En général, les fonctionnalités standard fournissent la garantie de base (sauf s'il y a quelque chose de mal avec ce que l'utilisateur fournit), et std::variantn'a aucune raison de casser cela. Je conviens que cela peut être rendu plus explicite dans le libellé de la norme, mais c'est essentiellement la façon dont les autres parties de la bibliothèque standard fonctionnent. Et pour info , P0088 était la proposition initiale.
LF
Merci. Il y a une spécification plus explicite à l'intérieur: if an exception is thrown during the call toT’s constructor, valid()will be false;donc cela a interdit cette "optimisation"
Flamefire
Oui. Spécifications de emplaceP0088 sousException safety
Flamefire
@Flamefire Semble être une divergence entre la proposition originale et la version votée. La version finale a été remplacée par le libellé "mai".
LF