Why aren't c++17's emplace() functions ref-qualified?

Viewed 445

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?

2 Answers

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.

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.

Related