2

I am attempting to move around a pointer by reference (T*&) between some template functions. Under certain conditions this pointer reference may get passed to a different function that accepts a void pointer reference (void*&). When I attempt to pass the templated type into the function accepting a void*&, it gives me the error:

error: cannot bind non-const lvalue reference of type 'void*&' to an rvalue of type 'void*'

This error is pretty self explanatory on its own. However I can't readily make sense of the error in context of the code. Here is a minimal reproduction of my error I was able to make in Godbolt (x86_64 gcc 10.2):

#include <iostream>
#include <type_traits>

void NonTempFunct(void*& Ptr)
{
    std::cout << "Pointer Value: " << Ptr << ".\n";
}

template<typename T, typename = std::enable_if_t< std::is_pointer_v<T> >>
void TempFunct(T& Param)
{
    std::cout << "Pointer found.\n";
    NonTempFunct( Param );
}

template<typename T, typename = std::enable_if_t< !std::is_pointer_v<T> >, typename = void>
void TempFunct(T& Param)
{
    std::cout << "Non pointer found.  No op.\n";
}

int main() 
{
    int Value = 50;
    int* pValue = &Value;

    TempFunct( pValue );

    return 0;
}

The error specifically complains about the invocation of NonTempFunct(void*&). As far as I am aware, there are no rvalues in this chain. They all have names and refer back to an automatically allocated variable.

I didn't stop here though, and fiddled with the code a bit. Using std::forward (NonTempFunct( std::forward<T&>(Param) );) or std::move (NonTempFunct( std::move(Param) );) when invoking NonTempFunct didn't change the error produced.

VERY curiously, when I switched the references in both TempFunct declarations to a universal reference (&&) the program did compile, however the wrong version was selected with SFINAE, suggesting the std::is_pointer_v<T> check failed with universal references.

The one thing that did work was a reinterpret_cast in the call to NonTempFunct (without universal references).

NonTempFunct( reinterpret_cast<void*&>(Param) );

That compiles. I fear I don't understand C++ well enough to make sense of these results. My specific questions are:

  1. Where is the rvalue from the initial error coming from?
  2. Why does the use of a universal reference cause std::is_pointer_v to fail?
  3. Why does a reinterpret_cast bypass these issues?
Mako_Energy
  • 352
  • 2
  • 19
  • Your argument types should be `void* Ptr, and T Param` not `void*& Ptr and T& Param` – Pepijn Kramer Jul 05 '22 at 05:56
  • A reference with a pointer to A is not implicit convertible to a reference with a pointer to B even if A is convertible to B. With a `reinterprete_cast`, you can make the compiler to accept nearly everything - regardless whether this is U.B. or not. – Scheff's Cat Jul 05 '22 at 05:57
  • Same principle, different take: [Why does const allow implicit conversion of references in arguments?](https://stackoverflow.com/questions/48576011/) – JaMiT Jul 05 '22 at 06:33
  • @PepijnKramer how do you know what the types should be? Can you create and answer explaining your logic? – Sqeaky Jul 05 '22 at 16:36

1 Answers1

3

Case 1

Here we discuss the reason for the mentioned error.

The problem is that param is an lvalue of type int* and it can be converted to a prvalue of type void* when passing it as the call argument in NonTempFunct( Param ); but the parameter of NonTempFunct is a non-const lvalue reference which cannot be bound to an rvalue.

Essentially, the result of the conversion(int*->void*) will be a prvalue and a non-const lvalue reference cannot be bound to that rvalue.

To solve this you can either make the parameter of NonTempFunct to be a const lvalue reference or simply a void* as shown below

Method 1

//----------------------vvvvv---------->added this
void NonTempFunct(void *const& Ptr)
{
    std::cout << "Pointer Value: " << Ptr << ".\n";
}

Working demo

Method 2

//----------------vvvvv---------->removed the reference
void NonTempFunct(void* Ptr)
{
    std::cout << "Pointer Value: " << Ptr << ".\n";
}

Working demo


Case 2

Here we discuss the reason when we use universal reference, the program compiles without any error.

When you make the function template's parameter to be T&& and use the call TempFunct( pValue ) then T is deduced to be int*& i.e., non const lvalue reference to a non const pointer to int.

This means that std::is_pointer_v<T> will be the same as std::is_pointer_v<int*&> which will be false. Demo.

This in turn means that the first overloaded version will be SFINAE'd OUT. And since the second version is viable(as it uses !std::is_pointer_v<T> which is the same as !std::is_pointer_v<int*&> and so is true ), it will be used and we will get the output Non pointer found. No op.

Jason
  • 36,170
  • 5
  • 26
  • 60
  • How does this interact with universal references as described in the original question? – Sqeaky Jul 05 '22 at 16:35
  • @Sqeaky See my updated answer(case 2 in particular) where i have explained why with universal references the program compiles. – Jason Jul 05 '22 at 17:53
  • The source of the rvalue being from an implicit conversion is something I am kicking myself for not thinking of, that makes sense. This result strongly indicates reinterpret_cast doesn't create an rvalue for the compiler to complain about, and that is supported by the cpp reference page on reinterpret_cast. So that explains why reinterpret_cast works. Sadly, in the production code this is based on I need to be able to write to both the pointer and underlying data, so neither of your suggested fixes can be used. But you have helped me understand nonetheless. – Mako_Energy Jul 05 '22 at 18:40
  • The universal reference is still weird to me. I'd normally expect when assessing types, the parts that compose ```T``` (i.e. ```int*```) from the parameter would be all that is evaluated, omitting the reference portion. If I did ```decltype(Param)```, THEN I'd expect to get ```int*&```. Can you explain why this is? Or is this a case of "just is"? – Mako_Energy Jul 05 '22 at 18:45
  • @Mako_Energy Are you asking why `decltype(Param)` is `int*&` for case 2 of universal reference? If yes, then it is because there is a rule for this in the standard. In particular, [here](https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers) you will find the following: *"If the expression initializing a universal reference is an lvalue, the universal reference becomes an lvalue reference."* The same can be found in the standard. – Jason Jul 06 '22 at 03:45