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

120

Question: 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:

  • gcc-trunk:

    <source>:12:33:   required from here <source>:7:5: error: forming reference to void     7 |     maybe_void(const T& v) requires(!std::is_void_v<T>) {}       |     ^~~~~~~~~~ 
  • clang-trunk:

    <source>:7:23: error: cannot form a reference to 'void'    maybe_void(const T& v) requires(!std::is_void_v<T>) {}                      ^ <source>:12:14: note: in instantiation of template class 'maybe_void<void>' requested here     auto v = maybe_void<void>(42);              ^ 
  • msvc-v19-latest:

    <source>(7): error C2182: 'v': illegal use of type 'void' <source>(12): note: see reference to class template instantiation 'maybe_void<void>' being compiled 

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.

Total Answers: 1

53

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

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.