C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Making the converting constructor of std::optional less greedy

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Thu, 7 Dec 2023 09:29:14 -0600
On Thu, 7 Dec 2023 at 08:46, Ville Voutilainen via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> On Thu, 7 Dec 2023 at 07:15, Egor via Std-Proposals
> <std-proposals_at_[hidden]> 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
};
}

Received on 2023-12-07 15:29:27