Home > Enterprise >  "Cannot form reference to void" error even with `requires(!std::is_void_v<T>)`
"Cannot form reference to void" error even with `requires(!std::is_void_v<T>)`

Time:10-05

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;
}

Demo

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.

  • Related