18

There is an ongoing debate about what optional and variant should do with reference types, particularly with regards to assignment. I would like to better understand the debate around this issue.

optional<T&> opt;
opt = i;
opt = j; // should this rebind or do i=j?

Currently, the decision is to make optional<T&> ill-formed and make variant::operator= ill-formed if any of the types is a reference type - to sidestep the argument and still give us most of the functionality.

What is the argument that opt = j should rebind the underlying reference? In other words, why should we implement optional like this:

template <class T>
struct optional<T&> {
    T* ptr = nullptr;

    optional& operator=(T& rhs) {
        ptr = &rhs;
        return *this;
    }
};
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 4
    Well, in your example, if `opt = i` binds a reference and then `opt = j` assigns through the reference, wouldn't that feel weird? – T.C. Oct 11 '16 at 19:18
  • 2
    You can hardly bind normal references using assignment. – T.C. Oct 11 '16 at 19:23
  • Especially weird if the `opt` came in as a function parameter, and it depends at runtime whether `opt = i;` assigns or binds – M.M Oct 11 '16 at 19:47
  • We already have the rebinding of `std::reference_wrapper` with `auto ref = std::ref(i); ref = j;` (because of the non-explicit constructor). So for coherency, the rebind seem more logical. – Jarod42 Oct 11 '16 at 20:37

1 Answers1

20

What is the argument that opt = j should rebind the underlying reference?

I don't know what "the argument" you're looking for is. But you've just presented "an argument" for it:

optional<T&> opt;
opt = i;
opt = j;

Now, pretend that the second and third lines are far from each other. If you're just reading the code, what would you expect opt = j to do? Or more to the point, why would you expect its behavior to differ from opt = i?

To have the behavior of a wrapper type differ so drastically based purely on its current state would be very surprising.

Furthermore, we already have a way to communicate that you want to change the value inside the optional. Namely: *opt = j. This works just as well for optional<T&> as it does for optional<T>.

The way optional works is very simple: it's a wrapper type. Like any currently existing wrapper types, operations on them affect the wrapper, not the thing being wrapped. To affect the thing being wrapped, you explicitly use * or -> or some other interface function.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 5
    Your last paragraph I find the most compelling - operations on the wrapper affect the wrapper. But the rest all makes sense too. – Barry Oct 11 '16 at 20:30
  • Actually, this is the argument for not using operator = of type T when assigning to engaged optional even when references are not involved. – Григорий Шуренков Oct 11 '16 at 21:02
  • 2
    @ГригорийШуренков Wat? There's no question of what `operator=` should do in that case. There's only one thing it could possibly mean. – Barry Oct 11 '16 at 21:55
  • There are two rather different operations: 'assign_value' (that will rebind the reference in the case of references) and 'reset_wrapper' used inside optional's operator=. If argument is that operation on optional should affect state of optional and not the state of its value the only thing operator = could possibly mean is 'reset_wrapper' - destroy current value completely if there's one and after that wrap the new value. – Григорий Шуренков Oct 12 '16 at 06:23
  • @ГригорийШуренков: To assign a value to an `optional` is to say, "I want the optional to hold this value." If the optional holds no object, then you want to copy/move construct into it. If the optional already holds an object, then you want to copy/move assign into the already held object, such that the object will now have the new value. There is no need to destroy the current object and create a new one. There is no useful difference between "destroy-and-copy-construct" and "copy-assign". Except of course for performance, which is why we do the latter. – Nicol Bolas Nov 02 '16 at 15:13
  • There's no useful difference until there is. Until copying into existing object has side-effects. Why do you want to suppress side-effects in one case but not the others? Also if "destroy-and-copy-construct" is the default behavior for optional then there's no need in a special case for optional, because there's no special case, – Григорий Шуренков Nov 02 '16 at 16:55
  • @ГригорийШуренков: "*if "destroy-and-copy-construct" is the default behavior for optional*" Then nobody would ever use the type, because the default behavior of `=` makes absolutely no sense. The only types that behave that way are type-erased wrappers like `function` or `any`. `optional` would be a poor wrapper type if it didn't forward what operations it could onto the wrapped object. – Nicol Bolas Nov 02 '16 at 16:59
  • Either wrapper should forward all operations to wrapped value if possible or operations on wrapper should not affect thing being wrapped. You cannot have both in a generic setting. – Григорий Шуренков Nov 02 '16 at 17:24
  • @ГригорийШуренков: You're making my point for me. `optional` is capable of forwarding `operator=` calls to the wrapped `T` if it is engaged, so it does so. Just like it forwards comparison operators and other things. By contrast, it would be difficult for `any` to forward such operations to the contained type due to type-erasure. So it does not. – Nicol Bolas Nov 02 '16 at 17:30
  • My point is that we should stick to one of two principles: 1) optional should forward all operations to wrapped value if possible 2) operations on optional should not affect wrapped value For optional It's impossible to stick to both of them. If you say that first principle is more important than the second, so be it. But then we should apply this principle for optional as well. It would be inconsistent for optional to adhere to second principle when optional adheres to first. – Григорий Шуренков Nov 02 '16 at 17:57
  • @ГригорийШуренков: And that's precisely the problem with `optional`. See, users *do not care* if `ot = t` for `optional` performs copy construction or copy assignment; they just want a copy to happen. However, users ***must care*** whether `ot = t` is going to perform a copy or store a reference to `t` for the case of `optional`. That changes the very meaning of what they're trying to do. And you *cannot know* which is going to happen without knowing whether `ot` is engaged or not. So it's better to avoid this case altogether. – Nicol Bolas Nov 02 '16 at 18:20
  • I'd argue that users should care even in the case of optional. It's rather easy not to care if it's optional or optional> the behavior of copy-assigment and copy-construction makes no big difference in this case (except the former is more efficient). However, there might be user-defined types that behave exactly as T&. Imagine user-written reference wrapper that forwards assignment to wrapped reference. Or imagine reactive_variable that notifies subscribers when its value changes via assignment. How optional should behave with such types in a perfect world? – Григорий Шуренков Nov 02 '16 at 18:32
  • @ГригорийШуренков: The fact that a user can put types with unusual copy-assignment behavior into `optional` does not justify allowing `optional`. – Nicol Bolas Nov 02 '16 at 18:50
  • I think the wrapper in this case should behave like the wrapped type. C++ does not allow uninitialized references and it does not allow rebinding references. This is then the expected behaviour for `optional`. – olq_plo Jan 16 '19 at 21:49
  • @olq_plo: Except that it isn't the behavior that it would have. Pretty much everyone who wants `optional` to exist would say that it should be possible to default construct one (which will be unengaged; if an `optional` cannot be unengaged, then what good is it?). And pretty much everyone who wants `optional` to exist would say that it should be possible to *rebind* one (even if that means `nullopt`-ing it and then giving it a new reference). – Nicol Bolas Jan 16 '19 at 22:20