Bitwize
Bitwize

Reputation: 11220

Why do functions disabled by C++20 `requires` clauses still cause ill-formed type errors?

Working with C++20's requires statements, I've noticed that using requires to selectively disable a function definition breaks if a type in that function would be ill-formed -- even though the function is not enabled.

The simplest example I've found for this is anything with a T& where T may be void, for example:

template <typename T>
struct maybe_void {
    maybe_void() requires(std::is_void_v<T>) = default;

    maybe_void(const T& v) requires(!std::is_void_v<T>) {}
    explicit maybe_void(int) {};
};

auto test() -> void {
    auto v = maybe_void<void>(42); // error -- sees 'const void&' constructor, even though it's disabled
}

All major compilers agree that this is an error:

Live Example


It was my understanding that requires was meant to work in cases like the above, but the various compilers seem to suggest otherwise. Why doesn't this work?


Note:

One possible workaround is to change the constructor to a constrained template instead:

template <typename U>
explicit maybe_void(const U& v) requires(!std::is_void_v<U> && std::same_as<T,U>);

Live Example

Although this works as a workaround, it doesn't explain why the requires clause doesn't prevent the otherwise disabled function from triggering the ill-formed issue in the first place.

Upvotes: 1

Views: 361

Answers (1)

Nicol Bolas
Nicol Bolas

Reputation: 473407

The problem is that the function signature is syntactically invalid. void const & is not a legitimate type in C++. So the compiler never gets to the requires clause for the function. It never gets to consider whether the function should or should not be discarded because the function is not a legal function signature.

The way this is generally dealt with is that you cull out void in the type. So you would need a separate specialization that allows void. Note that in most cases where a constructor would want to take a T const&, you'd have to do this anyway, because that function would almost certainly want to copy that T into a member variable or something. And you can't have a member variable of type void. So you'd still need a separate specialization.

If you do not otherwise need a specialization of your type to handle void, then you can create a type which, when given a T that is void results in an innocuous non-void type.

struct dont_use {};

template<typename T>
struct non_void { using type = T; };

template<>
struct non_void<void> { using type = dont_use; };

...

maybe_void(non_void_t<T> const& v) requires(!std::is_void_v<T>) {}

As seen here.

Upvotes: 3

Related Questions