On Thu, 7 Dec 2023 at 08:46, Ville Voutilainen via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
On Thu, 7 Dec 2023 at 07:15, Egor via Std-Proposals
<std-proposals@lists.isocpp.org> wrote:
>
> Hello!
>
> std::optional<T> has a really greedy converting constructor. It accepts
> almost any type that T is constructible from, and constructs a T from
> it, even if that type is also convertible to optional<T>. Example:
> https://gcc.godbolt.org/z/ncsYehW19
>
>      #include <iostream>
>      #include <optional>
>      #include <source_location>
>
>      struct A
>      {
>          template <typename T>
>          operator T()
>          {
>              std::cout <<
> std::source_location::current().function_name() << '\n';
>              return {};
>          }
>      };
>
>      int main()
>      {
>          std::optional<int> x(A{}); // (1) Calls `operator int`, not
> `operator optional<int>`.
>          // std::optional<int> y = A{}; // (2) Ambiguous.
>          x = A{}; // (3) Calls `operator int`, not `operator optional<int>`.
>      }
>
> This is very confusing in my eyes. std::optional is first type I see in
> the wild that doesn't play nice with templated conversion operators.
>
> I suggest we make this constructor of optional not participate in
> overload resolution if the argument is convertible to optional<T>.
>
> This would make (2) valid, and make all three lines call A::operator
> optional<T>.

It was an intentional design decision to give the T in optional<T> a
chance to convert directly
from any U, including U=optional<Z>, if it can, instead of wrapping
into an optional<Z> first
and then converting T from Z.

You may think that makes optional not play nice with templated
conversion operators, but
if we make the change you suggest, then someone will come in and say
that optional doesn't
play nice with templated conversion constructors.

We're not talking about U -> optional<Z> -> optional<T> though, we're talking about U -> optional<T> directly. Intuitively it seems that that should be preferred over U -> T -> optional<T>.

Another case that I think is ultimately related:

#include <functional>
int main() {
    std::copyable_function<void()> f;
    std::move_only_function<void()> g = f;
    if (g)
        g(); // oops, throws std::bad_function_call (and crashes)
}

If I'm writing my own type-erased function wrappers, I can check in my constructor (replacing move_only_function) whether the source object is a disengaged function wrapper. But I can't fix the constructor of move_only_function if replacing copyable_function, even if I know exactly how I want to convert to move_only_function.

namespace my {
concept FunctionWrapper = ...;
struct MoveOnlyFunction {
    MoveOnlyFunction(FunctionWrapper auto); // called
};
struct CopyableFunction {
    operator FunctionWrapper auto() const; // not called
};
}