5

C++17 introduced a new function signature for a few emplace_back() type functions (std::optional<> has one too), but they are not ref-qualified. This allows emplace_back() to be called on a temporary and bound to lvalue references, even though the lifetime of the object is not extended. Consider the following:

#include <vector>
#include <optional>
#include <iostream>

struct A {
   A(int i) : i(i) { std::cout << "constructor called: " << i << '\n'; }
   ~A() { std::cout << "destructor called\n"; }

   int i;
};

int main() {
    auto & a = std::optional<A>().emplace(5);
    std::cout << "a: " << a.i << '\n';
    auto & v = std::vector<A>().emplace_back(5);
    std::cout << "v: " << v.i << '\n';

    // This fails to compile, because value() *is*
    // ref-qualified so it cannot bind to an lvalue reference
    //auto & a2 = std::optional<A>(std::in_place, 5).value();

    auto const & a2 = std::optional<A>(std::in_place, 5).value();
    std::cout << "a2: " << a2.i << '\n';
}

The output:

constructor called: 5
destructor called
a: 5
constructor called: 5
destructor called
v: 0
constructor called: 5
destructor called
a2: 5

I couldn't find any existing bugs or questions related to this, but maybe I'm just missing something. In the case of std::optional<>::value(), it mostly works, but still allows binding to const lvalue references whilst not properly extending the contained type's lifetime.

Is there any reason why these functions are not ref-qualified, and why std::optional<>::value() doesn't properly extend the lifetime of the contained object when used on an rvalue?

John Drouhard
  • 1,209
  • 2
  • 12
  • 18
  • 1
    The same thing happens with the insert function. related: https://stackoverflow.com/questions/48923835/rvalue-ref-qualifiers-for-stl-containers – NathanOliver Jan 09 '19 at 19:21
  • 1
    Value categories are not good indicators of lifetime. There are cases where a temporary can be valid as long as the return references (ie: a valid case), and there are cases where the reference outlives its associated variable (ie: invalid case). – KABoissonneault Jan 09 '19 at 19:21
  • 4
    If you stop assuming lifetime extension is a feature (it's never really necessary), you will get less such surprises – KABoissonneault Jan 09 '19 at 19:23
  • [tag:rust] has life time in its types. – Jarod42 Jan 09 '19 at 20:17

2 Answers2

1

Next to nothing in the std library is ref-qualified.

As for value, it returns a rvalue reference. When you have an rvalue qualified method returning a reference to internal state, you have two choices.

You can return a value, or you can return an rvalue reference.

If you return a value, this means .value() on an rvalue optional does a move, while .value() on an lvalue optional does not. This could be surprising.

The cost of .value() returning an rvalue is that reference lifetime extension does not apply. The cost of .value() returning a copy on an rvalue is the cost of the move, plus the surprise that it behaves differently.

Both have downsides. I have a memory of it being a discussion point in the design of optional. If my memory is correct, that means the decision was made with open eyes.

Barring an extreme improvement to reference lifetime extension, functions on rvalues returning rvalues to internal state are always going to bind to const& and not extend the lifetime of the external object.

The real pain point in my experience is for(:) loops;

std::optional<std::vector<int>> try_get_vec();

for (int x : try_get_vec().value()) // I know the optional won't be empty

the above has a dangling reference in it.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I like the range-v3 approach to this problem of returning a `dangling` in similar cases. The only thing you can do with this type is calling `get_unsafe()` on it to get the actual iterator. E.g. in `const auto my2 = find(ints(1,3), 2);`, `my2` is dangling because the iterator it wraps refers to a temporary range which is deleted at the end of the complete statement. This is overly restrictive for `std::optional::value()`, but I'm sure it could be adapted to work with intended uses while still preventing things like range-based for loops. – Arne Vogel Jan 10 '19 at 15:20
-1

Is there any reason why these functions are not ref-qualified, and [...]

There's a lot of functions in the standard library that could be &-qualified but aren't. Calling emplace()-like methods on an rvalue is probably a good example of code that probably doesn't make a whole lot of sense.

On the flip side, there's probably not a lot of people trying to call emplace()-like methods on rvalues, so maybe the payoff for &-qualifying all these functions isn't worth the time.

[...] and why std::optional<>::value() doesn't properly extend the lifetime of the contained object when used on an rvalue?

It can't extend the lifetime. There's no such language facility. The choice is either to return a value (which would force a move you may not need, but would be safe) or return a reference (which avoids unnecessary moves, but relies on the programmer to use it safely). The library chose the latter.

Barry
  • 286,269
  • 29
  • 621
  • 977