7

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.

Tobias Hermann
  • 9,936
  • 6
  • 61
  • 134

2 Answers2

5

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;
}
rustyx
  • 80,671
  • 25
  • 200
  • 267
  • Item 27 does go into constraining forwarding reference overloads, which `std::optional` does. – Barry Aug 24 '17 at 18:56
  • Thank you, Rusty. I now understand. Do you think it makes sense to report something on open-std.org? – Tobias Hermann Aug 25 '17 at 08:48
  • I just gave it [a try](https://groups.google.com/a/isocpp.org/forum/embed/?place=forum/std-discussion&showsearch=true&showpopout=true&parenturl=https://isocpp.org/forums/iso-c-standard-discussion#!topic/std-discussion/r5CY7HnnD8Y). :-) – Tobias Hermann Aug 25 '17 at 10:33
4

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().

Barry
  • 286,269
  • 29
  • 621
  • 977
  • OK. If I understand correctly this means that every cast operator to `std::optional` in a user class will always be ignored, right? If so, I think the specification of `std::optional` should be changed to allow this. What do you think? – Tobias Hermann Aug 24 '17 at 12:34
  • @TobiasHermann No, I didn't say that. If you remove your `operator int()` then the constructor template would cease to be viable but the move constructor would work fine. – Barry Aug 24 '17 at 12:36
  • Yes, removing `operator int()` would change the result. But don't you think it is strange that the perfect match for `static_cast>(my_foo)` - i.e. `operator std::optional()` is ignored? – Tobias Hermann Aug 24 '17 at 12:40
  • @TobiasHermann But it's not the perfect match, the other one is a *better match*. Also, you keep saying ignored. It's not ignored - it's just less preferred. – Barry Aug 24 '17 at 12:45
  • Why is `operator int()` a better match than `operator std::optional()` in the case of `static_cast>`? From my user perspective the second operator matches a lot better. And the first one brings a loss of information with it. – Tobias Hermann Aug 24 '17 at 12:49
  • @TobiasHermann I went over that in the other answer - the constructor template is an exact match, but the move constructor invokes a conversion. If you want to file an issue to add more constraints to the constructor template, you can do so. It's oddly specific to have types that are convertible to both `T` and `optional` that moreover have different results. – Barry Aug 24 '17 at 12:51
  • OK, thanks. So that means prefering the ctor in favour of the user-defined conversion (thus outputting `2`) is - at least according to the standard - the correct behavior in this situaltion. It is just that I think the specification of `std::optional` in the standard should be changed so that the user-defined conversion is preferred. Did I now get this right? – Tobias Hermann Aug 24 '17 at 14:49