1

This is an academic question. The std::optional<T> type has a T && value() && method. I have the following definitions:

class A { ... };
void f(A &&a);

And the following program:

std::optional<A> optA;
optA = A(1337);
f(std::move(optA).value()); // OPTION 1
f(std::move(optA.value())); // OPTION 2
std::cout << optA.has_value() << std::endl;

Is there any meaningful difference between OPTION 1 and OPTION 2? For OPTION 1 will I have 1, 0 or unspecified as output? According to my tests has_value() remained true in both cases.

Is there any possible situation where value() && makes a difference over std::move and value()?

Notinlist
  • 16,144
  • 10
  • 57
  • 99
  • Re: `has_value()` remained `true`: this depends on `f()`. Do you actually alter `a` in `f()`? – lorro Jun 19 '19 at 16:19
  • 1
    @lorro It actually does not depend on `f` -- note that `f` gets an `A`, it doesn't get `optA`. `has_value()` would only change if `f` somehow accessed `optA` directly (as a global?) – Barry Jun 19 '19 at 16:58
  • @Barry : after moving out from an optional itself, it is in a valid but indetermined state (to my knowledge). Your implementation might leaveit as-is. Or it might set it to nullopt. Or any global value - highly unrealistic but possible, esp. with aggregate types. On contrary, when you access value(), it is guaranteed that the optional is kept as-is b/c it’s a non-movable reference. – lorro Jun 19 '19 at 17:43
  • @lorro Nothing here is moving out of the optional. – Barry Jun 19 '19 at 17:48
  • @Barry : std::move(optA).value() will choose value() && over value(). – lorro Jun 19 '19 at 17:49
  • 2
    @lorro Which moves a value stored in the optional. It doesn't move the optional. It is akin to `std::vector foo(100); auto x = std::move(foo[0]);` vs `auto y = std::move(foo);` -- one moves something stored in the vector, the other moves the vector. All of the operations are moving something *stored in* the optional, none move the optional. – Yakk - Adam Nevraumont Jun 19 '19 at 18:19
  • @Yakk-AdamNevraumont: Can you please point me where the standard says that `std::optional::value() &&` (note the `&&`) guarantees that `*this` is not empty after the call (i.e., that it does not flip the flag to describe if it's empty - note that it does not have to be the same flag as to destruct or not the object in the storage)? My current knowledge is that `value() &&` guarantees that the `optional` is in valid-but-indeterminate state, while `value()` guarantees that it remains non-empty _and_ the contained object is in valid-but-indeterminate state. – lorro Jun 19 '19 at 18:58
  • @lorro 23.6.3.5 [optional.observe]/16? As in, the spot where `value()&&` is defined? "Equivalent to `return bool(*this) ? std::move(*val) : throw bad_optional_access()`". `operator*` does not disengage the `optional`, and moving from the data within the optional may not change the engagement state of the optional. I could find the bit where "the standard library is not allowed to mess with you", but there is a general rule that when you interact with object A you cannot mess with object B unless the standard says so. – Yakk - Adam Nevraumont Jun 19 '19 at 19:05
  • @Yakk-AdamNevraumont : Thx, TIL something. So in this case the above can't be done by the compiler and thus the optional will keep its value (if it had one). Thanks! – lorro Jun 19 '19 at 19:09

4 Answers4

4

There's no difference between std::move(optA).value() and std::move(optA.value()). value() just returns a glvalue referring to the contained value, and either you can have value() return an lvalue which is then converted to an xvalue by std::move, or you can call std::move first and have value() give you an xvalue right away (in reality, the conversion from lvalue to xvalue will be occurring somewhere within the value() method itself). The ref-qualified overloads are obviously not very useful in this simple case, but can be useful when the optional was passed by forwarding reference O&& o, and you want std::forward<O>(o).value() to "do the right thing".

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
3
f(std::move(optA).value()); // OPTION 1

You "moved" optA, but this doesn't mean you changed it. Usually you shouldn't use value after it was "moved out", as it's in a valid but indeterminate state. std::move is just a type cast (exactly same as static_cast<A&&>(optA)). Moving constructor wasn't called because no new instance of std::optional<A> was created. Hence has_value returns true.

In this case T && value() && is called indeed.

f(std::move(optA.value())); // OPTION 2

T && value() && is not called here, because optA is not &&. So you get A& and cast it to A&& by std::move, then pass to f which presumably does nothing. optA wasn't changed and still reports that it contains value.

Andriy Tylychko
  • 15,967
  • 6
  • 64
  • 112
  • Your first sentence doesn't make sense. `std::move` does not move. `std::move(x)` **does not move x**. Stating that the OP "moved out" `optA` doesn't make sense (or, more accurately, is false). You clarify later, but you start out with something that isn't true. Confusing the issue with false statements isn't a great plan for an answer. – Yakk - Adam Nevraumont Jun 19 '19 at 18:15
  • @Yakk-AdamNevraumont: and you blame me? they called it `move` after all :) I tried to paraphrase the answer, and failed – Andriy Tylychko Jun 19 '19 at 19:56
1

Is there any possible situation where value() && makes a difference over std::move and value()?

Consider the following:

optional<A> func() {...}

void f(optional<A> opt) {...}
void g(A a) {...}

f(func());
g(func().value());

f's opt parameter will be initialized by move. Well technically, it will be initialized directly by the prvalue, but pre-C++17 means that it gets move-initialized. That initialization can get elided, but if it isn't, then it is done via move. Always.

But what about g's parameter? What should happen? Well, consider what this would do:

struct C {string s;};
C func2() {...}

void h(string s);

h(func2().s);

The parameter of h is initialized by move. Why? Because if you access a member subobject of a prvalue, the resulting expression is an xvalue, and is therefore eligible for movement without explicitly using std::move.

The && constructor of optional ensures that value works the same way. If you call it on a prvalue temporary, then it will return an xvalue, which can be moved from without an explicit std::move call. So in the original case, g's parameter is initialized by move, exactly as it would have if it were accessing a member subobject of a prvalue.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
0

I would assume both versions are the same, however, the answer of Nicol Bolas looks interesting.

Assuming they both result in an rvalue and the code doesn't matter, we don't have a reason to ignore readability.

  std::move(optA).value()

This suggests that you move the variable and you rely on the writer of the class to provide the right overloads. If missing, you might miss out.

Some linters also recognize this as variable that no longer should be touched, preventing you from use-after-move.

std::move(optA.value())

This however converts the return value to an rvalue. I mostly consider this code as a bug. Either the function returns by value (or const reference) and we wrote to much code. Otherwise, the function returns an lvalue and you potentially ruined the internal state of the variable.

With this, I recommend to consistently move the variable and trigger the move of a function call as a code smell. (Even for std::optional, where it shouldn't matter)

JVApen
  • 11,008
  • 5
  • 31
  • 67