Fundamentally, the reason that optional and variant don’t allow reference types is that there’s disagreement on what assignment (and, to a lesser extent, comparison) should do for such cases. optional is easier than variant to show in examples, so I’ll stick with that:
int i = 4, j = 5;
std::optional<int&> o = i;
o = j; // (*)
The marked line can be interpreted to either:
- Rebind
o, such that&*o == &j. As a result of this line, the values ofiandjthemselves remain changed. - Assign through
o, such&*o == &iis still true but nowi == 5. - Disallow assignment entirely.
Assign-through is the behavior you get by just pushing = through to T‘s =, rebind is a more sound implementation and is what you really want (see also this question, as well as a Matt Calabrese talk on Reference Types).
A different way of explaining the difference between (1) and (2) is how we might implement both externally:
// rebind
o.emplace(j);
// assign through
if (o) {
*o = j;
} else {
o.emplace(j);
}
The Boost.Optional documentation provides this rationale:
Rebinding semantics for the assignment of initialized optional references has been chosen to provide consistency among initialization states even at the expense of lack of consistency with the semantics of bare C++ references. It is true that
optional<U>strives to behave as much as possible as U does whenever it is initialized; but in the case whenUisT&, doing so would result in inconsistent behavior w.r.t to the lvalue initialization state.Imagine
optional<T&>forwarding assignment to the referenced object (thus changing the referenced object value but not rebinding), and consider the following code:optional<int&> a = get(); int x = 1 ; int& rx = x ; optional<int&> b(rx); a = b ;What does the assignment do?
If
ais uninitialized, the answer is clear: it binds tox(we now have another reference tox). But what if a is already initialized? it would change the value of the referenced object (whatever that is); which is inconsistent with the other possible case.If
optional<T&>would assign just likeT&does, you would never be able to use Optional’s assignment without explicitly handling the previous initialization state unless your code is capable of functioning whether after the assignment,aaliases the same object asbor not.That is, you would have to discriminate in order to be consistent.
If in your code rebinding to another object is not an option, then it is very likely that binding for the first time isn’t either. In such case, assignment to an uninitialized
optional<T&>shall be prohibited. It is quite possible that in such a scenario it is a precondition that the lvalue must be already initialized. If it isn’t, then binding for the first time is OK while rebinding is not which is IMO very unlikely. In such a scenario, you can assign the value itself directly, as in:assert(!!opt); *opt=value;
Lack of agreement on what that line should do meant it was easier to just disallow references entirely, so that most of the value of optional and variant can at least make it for C++17 and start being useful. References could always be added later – or so the argument went.