Reputation: 11220
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
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>);
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
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>) {}
Upvotes: 3