web-dev-qa-db-fra.com

Pourquoi les références sont-elles interdites dans std :: variant?

J'utilise boost::variant beaucoup et je le connais assez bien. boost::variant ne restreint en aucun cas les types bornés, en particulier, ils peuvent être des références:

#include <boost/variant.hpp>
#include <cassert>
int main() {
  int x = 3;
  boost::variant<int&, char&> v(x); // v can hold references
  boost::get<int>(v) = 4; // manipulate x through v
  assert(x == 4);
}

J'ai un vrai cas d'utilisation pour utiliser une variante de références comme vue d'autres données.

J'ai ensuite été surpris de constater que std::variant n'autorise pas les références en tant que types bornés, std::variant<int&, char&> ne compile pas et il dit ici explicitement:

Une variante n'est pas autorisée à contenir des références, des tableaux ou le type void.

Je me demande pourquoi cela n'est pas autorisé, je ne vois pas de raison technique. Je sais que les implémentations de std::variant et boost::variant sont différents, alors peut-être que cela a à voir avec ça? Ou les auteurs ont-ils pensé que c'était dangereux?

PS: je ne peux pas vraiment contourner la limitation de std::variant en utilisant std::reference_wrapper, car l'encapsuleur de référence n'autorise pas l'affectation à partir du type de base.

#include <variant>
#include <cassert>
#include <functional>

int main() {
  using int_ref = std::reference_wrapper<int>;
  int x = 3;
  std::variant<int_ref> v(std::ref(x)); // v can hold references
  static_cast<int&>(std::get<int_ref>(v)) = 4; // manipulate x through v, extra cast needed
  assert(x == 4);
}
18
olq_plo

Fondamentalement, la raison pour laquelle optional et variant n'autorisent pas les types de référence est qu'il existe un désaccord sur ce que l'affectation (et, dans une moindre mesure, la comparaison) devrait faire dans de tels cas. optional est plus facile que variant à afficher dans les exemples, je vais donc m'en tenir à cela:

int i = 4, j = 5;
std::optional<int&> o = i;
o = j; // (*)

La ligne marquée peut être interprétée comme:

  1. Reliez o, de telle sorte que &*o == &j. À la suite de cette ligne, les valeurs de i et j elles-mêmes restent modifiées.
  2. Attribuer via o, tel &*o == &i est toujours vrai mais maintenant i == 5.
  3. Interdisez entièrement l'affectation.

L'assignation est le comportement que vous obtenez en appuyant simplement sur = à T's =, rebind est une implémentation plus solide et c'est ce que vous voulez vraiment (voir aussi cette question , ainsi qu'un discours de Matt Calabrese sur Types de référence ).

Une façon différente d'expliquer la différence entre (1) et (2) est de savoir comment nous pouvons implémenter les deux en externe:

// rebind
o.emplace(j);

// assign through
if (o) {
    *o = j;
} else {
    o.emplace(j);
}

La documentation Boost.Optional fournit cette justification:

Relier la sémantique pour l'affectation de initialisé les références facultatives ont été choisies pour assurer la cohérence entre les états d'initialisation même au détriment de manque de cohérence avec la sémantique des références C++ nues. C'est vrai que optional<U> s'efforce de se comporter autant que possible comme le fait U chaque fois qu'il est initialisé; mais dans le cas où U est T&, cela entraînerait un comportement incohérent par rapport à l'état d'initialisation lvalue.

Imaginez optional<T&> en transférant l'affectation à l'objet référencé (modifiant ainsi la valeur de l'objet référencé mais pas la liaison), et considérez le code suivant:

optional<int&> a = get();
int x = 1 ;
int& rx = x ;
optional<int&> b(rx);
a = b ;

Que fait la mission?

Si a est non initialisé, la réponse est claire: il se lie à x (nous avons maintenant une autre référence à x). Mais que se passe-t-il si a est déjà initialisé? cela changerait la valeur de l'objet référencé (quel qu'il soit); ce qui est incompatible avec l'autre cas possible.

Si optional<T&> affecterait comme T& le fait, vous ne pourrez jamais utiliser l'affectation d'Optional sans gérer explicitement l'état d'initialisation précédent à moins que votre code ne soit capable de fonctionner que ce soit après l'affectation, a alias le même objet que b ou ne pas.

Autrement dit, il faudrait faire de la discrimination pour être cohérent.

Si dans votre code, la liaison à un autre objet n'est pas une option, il est très probable que la liaison pour la première fois ne l'est pas non plus. Dans ce cas, affectation à un non initialiséoptional<T&> est interdite. Il est tout à fait possible que dans un tel scénario, il soit indispensable que la valeur l soit déjà initialisée. Si ce n'est pas le cas, la liaison pour la première fois est OK alors que la reliure ne l'est pas, ce qui est très improbable. Dans un tel scénario, vous pouvez affecter directement la valeur elle-même, comme dans:

assert(!!opt);
*opt=value;

Le manque d'accord sur ce que cette ligne devrait faire signifiait qu'il était plus facile de simplement interdire complètement les références, de sorte que la plupart de la valeur de optional et variant puisse au moins être utilisée pour C++ 17 et commencer à être utile. Des références pourraient toujours être ajoutées plus tard - du moins l'argument est-il allé.

16
Barry