C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Disambiguating certain constructions of std::optional

From: Jonathan Wakely <cxx_at_[hidden]>
Date: Fri, 7 Feb 2025 13:24:38 +0000
On Mon, 3 Feb 2025 at 19:41, Marc Edouard Gauthier via Std-Proposals <
std-proposals_at_[hidden]> wrote:

> We’ve encountered a limitation of the C++ library that doesn’t seem
> readily fixable without modifying the library, in turn requiring a
> corresponding small fix to the C++ standard.
>
> Does the following look right?
>
>
>
> For example, the following https://godbolt.org/z/f45bPd4sz
>
> fails to compile due to an ambiguous conversion:
>
>
>
>
>
> // Implicitly auto-converts to anything.
>
> static const struct default_value_t {
>
> template <class T> operator T() const { return T {}; }
>
> } default_value {};
>
>
>
> int main() {
>
> std::optional<int> a = default_value; // fails to compile
>
> return 0;
>
> }
>
>
>

Why is this something we need to support?
Just because we could support it, doesn't mean we need to.

Why is using default_value better than using {} ?




> GCC 14.2 output for example (similar on clang):
>
>
>
> *<source>:* In function '*int main*()':
>
> *<source>:13:28:* *error: *conversion from '*const default_value_t*' to '
> *std::optional<int>*' is ambiguous
>
> 13 | std::optional<int> a = *default_value*;
>
> | *^~~~~~~~~~~~~*
>
> *<source>:7:5:* *note: *candidate: '*default_value_t::operator T*() const
> [with T = std::optional<int>]'
>
> 7 | *operator* T() const
>
> | *^~~~~~~~*
>
> In file included from *<source>:1*:
>
> */opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/optional:745:9:* *note:
> *candidate: '*constexpr std::optional<_Tp>::optional*(_Up&&) [with _Up =
> const default_value_t&; typename
> std::enable_if<__and_v<std::__not_<std::is_same<std::optional<_Tp>,
> typename std::remove_cv<typename std::remove_reference<_Up>::type>::type>
> >, std::__not_<std::is_same<std::in_place_t, typename
> std::remove_cv<typename std::remove_reference<_Up>::type>::type> >,
> std::is_constructible<_Tp, _Up>, std::is_convertible<_Up, _Tp> >,
> bool>::type <anonymous> = true; _Tp = int]'
>
> 745 | *optional*(_Up&& __t)
>
> | *^~~~~~~~*
>
>
>
>
>
> The simplest fix appears to change this line of the standard:
>
>
> https://github.com/cplusplus/draft/blob/5bfd514aa5d9a19d1dd0f4e874412c210a1893ce/source/utilities.tex#L3538
>
> from:
>
> !is_convertible_v<U, T>
>
> to :
>
> !is_convertible_v<U, T> || is_convertible_v<U, optional<T>>
>
>
>

What's the logical reason for making this change? Why would we want this
constructor to be enabled-but-explicit for an argument of this type?

"It makes my use case compile" is not what I'm looking for. I want to know
why changing the explicit-ness of optional(U&&) is a sensible thing to do
here, in general, not just for your particular use case.




>
>
> Doing the associated edit on clang/llvm and glibc implementations of
> std::optional appears to fix the issue.
>
> As a quick way to run a test suite, a similar edit of the TartanLLama
> implementation of optional https://github.com/TartanLlama/optional
>
> also works, and passes its test suite. Specifically, the change there was
> this:
>
>
>
> /// Constructs the stored value with `u`.
>
> template <
>
> class U = T,
>
> - detail::enable_if_t<std::is_convertible<U &&, T>::value> * =
> nullptr,
>
> + detail::enable_if_t<std::is_convertible<U &&, T>::value &&
>
> + !std::is_convertible_v<U &&, optional<T>>> * =
> nullptr,
>
> detail::enable_forward_value<T, U> * = nullptr>
>
> constexpr optional(U &&u) : base(in_place, std::forward<U>(u)) {}
>
>
>
> template <
>
> class U = T,
>
> - detail::enable_if_t<!std::is_convertible<U &&, T>::value> * =
> nullptr,
>
> + detail::enable_if_t<!std::is_convertible<U &&, T>::value ||
>
> + std::is_convertible_v<U &&, optional<T>>> * =
> nullptr,
>
> detail::enable_forward_value<T, U> * = nullptr>
>
> constexpr explicit optional(U &&u) : base(in_place, std::forward<U>(u))
> {}
>
>
>
>
>
> Is submitting a defect report appropriate? How might we proceed?
>

I don't think there's a defect here, the std::optional constructor overload
set is working as designed, and I would be very nervous about making
changes to it without much stronger rationale.

Received on 2025-02-07 13:24:54