Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is a cast operator to std::optional ignored?

This code

#include <iostream>
#include <optional>

struct foo
{
    explicit operator std::optional<int>() {
        return std::optional<int>( 1 );
    }
    explicit operator int() {
        return 2;
    }
};

int main()
{
    foo my_foo;

    std::optional<int> my_opt( my_foo );
    std::cout << "constructor: " << my_opt.value() << std::endl;

    my_opt = static_cast<std::optional<int>>(my_foo);
    std::cout << "static_cast: " << my_opt.value() << std::endl;
}

produces the following output

constructor: 2
static_cast: 2

in Clang 4.0.0 and in MSVC 2017 (15.3). (Let's ignore GCC for now, since it's behavior seems to be buggy in that case.)

Why is the output 2? I would expect 1. The constructors of std::optional seem to prefer casting to the inner type (int) despite the fact that a cast to the outer type (std::optional<int>) is available. Is this correct according to the C++ standard? If so, is there a reason the standard does not dictate to prefer an attempt to cast to the outer type? I would find this more reasonable and could imagine it to be implemented using enable_if and is_convertible to disable the ctor if a conversion to the outer type is possible. Otherwise every cast operator to std::optional<T> in a user class - even though it is a perfect match - would be ignored on principle if there is also one to T. I would find this quite obnoxious.

I posted a somewhat similar question yesterday but probably did not state my problem accurately, since the resulting discussion was more about the GCC bug. That's why I am asking again more explicitly here.

like image 411
Tobias Hermann Avatar asked Aug 24 '17 12:08

Tobias Hermann


2 Answers

In case Barry's excellent answer still isn't clear, here's my version, hope it helps.

The biggest question is why isn't the user-defined conversion to optional<int> preferred in direct initialization:

    std::optional<int> my_opt(my_foo);

After all, there is a constructor optional<int>(optional<int>&&) and a user-defined conversion of my_foo to optional<int>.

The reason is the template<typename U> optional(U&&) constructor template, which is supposed to activate when T (int) is constructible from U and U is neither std::in_place_t nor optional<T>, and direct-initialize T from it. And so it does, stamping out optional(foo&).

The final generated optional<int> looks something like:

class optional<int> {
    . . .
    int value_;
    . . .
    optional(optional&& rhs);
    optional(foo& rhs) : value_(rhs) {}
    . . .

optional(optional&&) requires a user-defined conversion whereas optional(foo&) is an exact match for my_foo. So it wins, and direct-initializes int from my_foo. Only at this point is operator int() selected as a better match to initialize an int. The result thus becomes 2.

2) In case of my_opt = static_cast<std::optional<int>>(my_foo), although it sounds like "initialize my_opt as-if it was std::optional<int>", it actually means "create a temporary std::optional<int> from my_foo and move-assign from that" as described in [expr.static.cast]/4:

If T is a reference type, the effect is the same as performing the declaration and initialization
T t(e); for some invented temporary variable t ([dcl.init]) and then using the temporary variable as the result of the conversion. Otherwise, the result object is direct-initialized from e.

So it becomes:

    my_opt = std::optional<int>(my_foo);

And we're back to the previous situation; my_opt is subsequently initialized from a temporary optional, already holding a 2.

The issue of overloading on forwarding references is well-known. Scott Myers in his book Effective Modern C++ in Chapter 26 talks extensively about why it is a bad idea to overload on "universal references". Such templates will tirelessly stamp out whatever the type you throw at them, which will overshadow everything and anything that is not an exact match. So I'm surprised the committee chose this route.


As to the reason why it is like this, in the proposal N3793 and in the standard until Nov 15, 2016 it was indeed

  optional(const T& v);
  optional(T&& v);

But then as part of LWG defect 2451 it got changed to

  template <class U = T> optional(U&& v);

With the following rationale:

Code such as the following is currently ill-formed (thanks to STL for the compelling example):

optional<string> opt_str = "meow";

This is because it would require two user-defined conversions (from const char* to string, and from string to optional<string>) where the language permits only one. This is likely to be a surprise and an inconvenience for users.

optional<T> should be implicitly convertible from any U that is implicitly convertible to T. This can be implemented as a non-explicit constructor template optional(U&&), which is enabled via SFINAE only if is_convertible_v<U, T> and is_constructible_v<T, U>, plus any additional conditions needed to avoid ambiguity with other constructors...

In the end I think it's OK that T is ranked higher than optional<T>, after all it's a rather unusual choice between something that may have a value and the value.

Performance-wise it is also beneficial to initialize from T rather than from another optional<T>. An optional is typically implemented as:

template<typename T>
struct optional {
    union
    {
        char dummy;
        T value;
    };
    bool has_value;
};

So initializing it from optional<T>& would look something like

optional<T>::optional(const optional<T>& rhs) {
  has_value = rhs.has_value;
  if (has_value) {
    value = rhs.value;
  }
}

Whereas initializing from T& would require less steps:

optional<T>::optional(const T& t) {
  value = t;
  has_value = true;
}
like image 121
rustyx Avatar answered Nov 03 '22 01:11

rustyx


A static_cast is valid if there is an implicit conversion sequence from the expression to the desired type, and the resulting object is direct-initialized from the expression. So writing:

my_opt = static_cast<std::optional<int>>(my_foo);

Follows the same steps as doing:

std::optional<int> __tmp(my_foo); // direct-initialize the resulting
                                  // object from the expression
my_opt = std::move(__tmp);        // the result of the cast is a prvalue, so move

And once we get to construction, we follow the same steps as my previous answer, enumerating the constructors, which ends up selecting the constructor template, which uses operator int().

like image 23
Barry Avatar answered Nov 03 '22 00:11

Barry