Why does std::optional::value_or() take a U&& rather than T&&?
Asked Answered
O

3

21

On cppreference, we can see std::optional takes a default value of U&& rather than T&&.

It makes me incapable of writing the following code:

std::optional<std::pair<int, int>> opt;
opt.value_or({1, 2}); // does not compile
opt.value_or(std::make_pair(1, 2)); // compiles

However, I find there is no benefit to using U&&, since U must be convertible to T here.

So, consider the following code, if we have some type U which differs from T, then there will be no perfect match. However, by performing an implicit cast, we can still resolve our call:

template< class U >
constexpr T value_or( T&& default_value ) const&;

I have the following code to test if a template function can take an argument which needs an extra implicit casting to make a perfect match, and it compiles:

#include <cstdio>
#include <optional>
#include <map>

struct A {
    int i = 1;
};

struct B {
    operator A() const {
        return A{2};
    }
};

template <typename T>
struct C {
    void f(T && x) {
        printf("%d\n", x.i);
    }
};

int main() {
    auto b = B();
    C<A> c;
    c.f(b);
}
Orlina answered 24/5, 2023 at 12:50 Comment(6)
To avoid to do extra construction with call similar to std::optional<std::string>("some string").value_or("c-string");Incrocci
The answers explain why the proposed change is wrong. But the correct change would be to add the default template argument: typename U = T. No idea why that wasn't done.Heretofore
Unrelated, but simplifies things: opt.value_or(std::pair{1, 2});Transfuse
Another unrelated comment: This Is is a great Q&A. It's a very good question with three very good answers and I've upvoted everything. The only pity is that it shows up on the list of "Unanswered" questions.Transfuse
@TedLyngmo Do I need to accept one of these answers? I find it hard to pick the best because I think all of them are very good...Orlina
@Orlina No, you don't need to. It's just that the question shows up in the list of unanswered questions all the time and this question seems particularly well answered :-) I suggest upvoting them all if you find them all good and then picking one that stands out a little ... even if just a tad. Just a suggestion.Transfuse
K
34

I find there it no benefit by using U&&, since U must be convertible to T here.

U must be convertible to T, but a conversion can be expensive. Taking a forwarding reference (U&&) avoids performing that conversion if the argument is not used (if the object contains a value).

The same cppreference page says value_or is equivalent to:
bool(*this) ? **this : static_cast<T>(std::forward<U>(default_value)).
Here, static_cast<T> performs the conversion, but it is only performed if bool(*this) is false.

Knighterrantry answered 24/5, 2023 at 13:0 Comment(0)
E
21

It is to allow perfect forwarding. If the signature were

constexpr T value_or( T&& default_value ) const&;

then T is not something that will be deduced, it is known from the class instantiation, meaning T&& default_value is just a plain rvalue reference to whatever T is.

By using

template< class U >
constexpr T value_or( U&& default_value ) const&;

The U needs to be deduced making it a forwarding reference.

Ejector answered 24/5, 2023 at 12:55 Comment(10)
or it can be simply overloaded?Minatory
@appleapple It could be, but Nelfeal points out in there answer why you wouldn't want to do that.Ejector
true (was about to edit my comment to mention @Jarod42's comment in OP).Minatory
@appleapple actually has a good point, you could provide both the template and additional overloads for T (like constexpr T value_or( T&& default_value ) const&;). Only perfect matches would select the non-template overloads. However, I doubt that would offer much value.Knighterrantry
Ok, so actually I can think of U&& as a universal reference. That makes sense.Orlina
@Orlina Yes. They've changed the name to forwarding reference.Ejector
That doesn't explain why the type isn't defaulted to T though, which would allow the example to compile...Suppositious
@Suppositious I'm not sure why it doesn't have a default set. It could be an oversight but it almost feels intentional. I'll try to see if something is mentioned in the optional proposalEjector
@Suppositious After finding this, it looks like it just might not have been considered. The original proposal is back from 2005-08-29 so they might not even known about the whole { ... } being a non-deduced types, or that the syntax was even going to be a thing.Ejector
@NathanOliver-IsonStrike That sounds reasonable. Too bad it didn't trigger a change when std::expected::value_or was added though. Instead, the proposal for that simply says: "The function member value_or() has the same semantics than optional".Transfuse
L
10

As a concrete example

template<class T>
struct constructor {
  operator T()const{
    return f();
  }
  std::function<T()> f;
  constructor( std::function<T()> fin = []{return T{};} ):
    f(std::move(fin))
  {}
};
template<class F>
constructor(F) -> constructor<std::invoke_result_t<F>>;

Now I have an std::optional<std::vector<int>> bob;. I can do:

auto v = bob.value_or( constructor{ []{ return std::vector<int>(10000); } } );

here, if bob has a value, we return it. Otherwise, we create a 10,000 element vector and return that.

Because we defer the conversion from U&& to T for the false branch, we don't have to allocate the 10,000 sized vector unless we need to return it.

Now, I find that adding a T&& overload in these cases is well worth it, and the standard library doesn't do it enough, mostly for the reason you describe -- it permits {} based construction.

Libation answered 24/5, 2023 at 13:34 Comment(3)
Ok, I think I get your point. If I use T&&, then the conversion must be done before we pass the default_value to value_or. However, we can defer the conversion to when we really needs it if we use U&&.Orlina
However, I think a better idea is to use or_else in this case, which is also recommended way in other languages like Rust.Orlina
@Orlina or_else doesn't exist in C++17 though - but it will come in C++23.Transfuse

© 2022 - 2024 — McMap. All rights reserved.