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);
}
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:
o
, de telle sorte que &*o == &j
. À la suite de cette ligne, les valeurs de i
et j
elles-mêmes restent modifiées.o
, tel &*o == &i
est toujours vrai mais maintenant i == 5
.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
estT&
, 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 commeT&
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 queb
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é.