I'm writing a pointer class and overloading the dereference operator operator*
, which returns a reference to the pointed-to object. When the pointed-to type is not void this is fine, but we cannot create a reference to void, so I'm trying to disable the operator*
using a requires clause when the pointed-to type is void.
However, I'm still getting compiler errors from GCC, Clang, and MSVC for the void case even though it does not satisfy the requires clause.
Here is a minimal example and compiler explorer link (https://godbolt.org/z/xbo5v3d1E).
#include <iostream>
#include <type_traits>
template <class T>
struct MyPtr {
T* p;
T& operator*() requires(!std::is_void_v<T>)
{
return *p;
}
};
int main() {
int x = 42;
MyPtr<int> i_ptr{&x};
*i_ptr = 41;
MyPtr<void> v_ptr{&x};
std::cout << *static_cast<int*>(v_ptr.p) << '\n';
std::cout << x << '\n';
return 0;
}
And here is the error (in Clang):
<source>:7:6: error: cannot form a reference to 'void'
T& operator*()
^
<source>:20:17: note: in instantiation of template class 'MyPtr<void>' requested here
MyPtr<void> v_ptr{&x};
^
1 error generated.
ASM generation compiler returned: 1
<source>:7:6: error: cannot form a reference to 'void'
T& operator*()
^
<source>:20:17: note: in instantiation of template class 'MyPtr<void>' requested here
MyPtr<void> v_ptr{&x};
^
1 error generated.
Execution build compiler returned: 1
However, if I change the return type of operator*
from T&
to auto&
, then it works in all 3 compilers. If I use trailing return type auto ... -> T&
I also get errors in all 3 compilers.
Is this a triple compiler bug, user error, or is this intended behavior?
CodePudding user response:
The requires
clause doesn't matter because T
is a parameter of the class template. Once T
is known, the class can be instantiated, but if T
is void
, that instantiation fails because of the member function signature.
You can either put that requires
on the entire class, or make the member function a template like this:
template<typename U = T>
U& operator*() requires(!std::is_void_v<U> && std::is_same_v<T, U>)
{
return *p;
}
Making the return type auto&
is almost the same thing: the return type is deduced by replacing auto
with an imaginary type template parameter U
and then performing template argument deduction. Note that the version above with requires
makes the compilation error clear if you try to use this function with U=void
: GCC says template argument deduction/substitution failed: constraints not satisfied
.
I don't think there is a way to reproduce exactly what an auto&
return type does by making the function a template. Something like this might come close:
template<typename U = T>
std::enable_if_t<!std::is_void_v<T>, U>& operator*()
{
return *p;
}
Compare what you're trying with the equivalent using std::enable_if
(without concepts):
template<std::enable_if_t<!std::is_void_v<T>, bool> = true>
T& operator*()
{
return *p;
}
This will give you an error like no type named 'type' in 'struct std::enable_if<false, bool>'
, because SFINAE wouldn't work in this situation where T
is not a parameter of the function template.
Technically, you can also change the return type depending on whether T
is void
, but this is probably a bad idea:
using R = std::conditional_t<std::is_void_v<T>, int, T>;
R& operator*()
{
// calling this with T=void will fail to compile
// 'void*' is not a pointer-to-object type
return *p;
}
CodePudding user response:
In addition to the Nelfeal's answer, let me give an alternative solution. The problem is not in the dependence of requires
condition on T
, but is in the return type T&
. Let's use a helper type trait:
std::add_lvalue_reference_t<T> operator*()
requires(!std::is_void_v<T>)
{
...
}
It works because std::add_lvalue_reference_t<void> = void
, which makes operator*()
signature valid for T = void
.