J'ai récemment suivi une discussion sur Reddit qui a conduit à une belle comparaison de l' std::visit
optimisation 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_I
avec les argumentsstd::forward<Args>(args)....
Si une exception est levée,*this
peut 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_copyable
exclut 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 emplace
ne 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»?
la source
might/may
libellé, car la norme ne précise pas quelle est l'alternative.there is no way to detect the difference
.Oui.
emplace
doit 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).variant
doit 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 typeemplace
n'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.
Oui, si l'affectation de déplacement ne produit aucun effet observable, ce qui est le cas pour les types trivialement copiables.
la source
std::variant
n'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.if an exception is thrown during the call toT’s constructor, valid()will be false;
donc cela a interdit cette "optimisation"emplace
P0088 sousException safety