Search code examples
c++c++11rvalue-referencetemplate-argument-deduction

Reference to global functions not deduced correctly


I am using gcc 12.2 and find that the following code compiles and produces weird results try it in Godbolt. (P.S. Switching to clang shows the same result)

#include <iostream>
void global() { /*std::cout << "global()" << std::endl;*/ }

template <typename T> // universal reference
void invoke(T&& func, const std::string& tag) { 
    if constexpr (std::is_rvalue_reference_v<T>) {
        std::cout << "rvalue reference " << tag << std::endl;
    }
    else if constexpr (std::is_lvalue_reference_v<T>) {
        std::cout << "lvalue reference " << tag << std::endl;
    }
    else {
        std::cout << "non-reference " << tag << std::endl;
    }
    func(); 
}

template <typename T>
struct Test {
    Test(T&& x, const std::string& tag) // rvalue reference, not universal reference 
    {
        if constexpr (std::is_rvalue_reference_v<T>) {
            std::cout << "rvalue reference " << tag << std::endl;
        }
        else if constexpr (std::is_lvalue_reference_v<T>) {
            std::cout << "lvalue reference " << tag << std::endl;
        }
        else {
            std::cout << "non-reference " << tag << std::endl;
        }
    }
};

int main() {
    int && x = 3;
    using RRef = void (&&)();
    RRef rref = global;
    using LRef = void (&)();
    LRef lref = global;

    std::cout << "RRef       is rvalue reference " << std::is_rvalue_reference_v<RRef> << std::endl;
    std::cout << "rref       is rvalue reference " << std::is_rvalue_reference_v<decltype(rref)> << std::endl;
    std::cout << "move(rref) is rvalue reference " << std::is_rvalue_reference_v<decltype(std::move(rref))> << std::endl;
    std::cout << "x          is rvalue reference " << std::is_rvalue_reference_v<decltype(x)> << std::endl;
    std::cout << "move(x)    is rvalue reference " << std::is_rvalue_reference_v<decltype(std::move(x))> << std::endl;

    std::cout << "==== invoke ==== " << std::endl;
    invoke(global, "global");
    invoke(rref, "rref");
    invoke(std::move(rref), "rref2");
    invoke(lref, "lref");
    invoke(std::move(lref), "lref2");

    std::cout << "==== Test ==== " << std::endl;
    Test(global, "global");
    Test(rref, "rref");
    Test(std::move(rref), "rref2");
    Test(lref, "lref");
    Test(std::move(lref), "lref2");

    std::cout << "==== Test int ==== " << std::endl;
    // Test(x, "x");  // cannot bind lvalue to rvalue-reference
    Test(std::move(x), "move(x)");
}

The output is as following for gcc 12.2:

RRef       is rvalue reference 1
rref       is rvalue reference 1
move(rref) is rvalue reference 0   // why is this no longer rvalue reference
x          is rvalue reference 1
move(x)    is rvalue reference 1
==== invoke ==== 
lvalue reference global  // why are they all lvalue reference
lvalue reference rref
lvalue reference rref2
lvalue reference lref
lvalue reference lref2
==== Test ==== 
non-reference global  // why they are non-reference
non-reference rref
non-reference rref2
non-reference lref
non-reference lref2
==== Test int ==== 
non-reference move(x)

Could you please explain why we get the following output:

  1. std::move(rref) is an lvalue reference while std::move(x) is an rvalue reference
  2. when passing the functions to invoke, which accepts universal reference, the deduced type are all lvalue-reference, indicating lvalue references are passed to the invoke
  3. when passing the functions to Test, which accepts only rvalue references, the various inputs are all accepted, indicating they are all rvalue references.

Passing reference of int behaves normal, while passing reference of function behaves quite wierd.


Solution

    1. Even though std::move(x) is an rvalue reference, std::move(rref) is an lvalue because it is a special case in the standard. From C++14 standard [expr.call/p10]:

      A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.

      This is explained in detail in this answer.

    2. When passing the values to invoke(), you pass them either

      • Using their variable names (which are lvalues):
        invoke(global, "global");
        invoke(rref, "rref");
        
      • Or by means of std::move(ref-to-function) like here:
        invoke(std::move(rref), "rref2");
        
        This is also an lvalue because of a special case mentioned in (1).

      Thus, the template argument binds to lvalue in all cases.

    3. Please note that your Test constructor prints info about the template argument type T and not about the type of the parameter x (which is T&&). That's why you see "non-reference" where you expect "rvalue reference". If we replace the code to print info about decltype(x) instead of T we get the expected "rvalue reference" result.

      As to why the rvalue reference in Test constructor accepts the lvalue inputs. It's again a special case in the standard that permits binding of rvalue references to function lvalues. From C++14 standard [over.ics.ref/3]:

      Except for an implicit object parameter, for which see [over.match.funcs], a standard conversion sequence cannot be formed if it requires binding an lvalue reference other than a reference to a non-volatile const type to an rvalue or binding an rvalue reference to an lvalue other than a function lvalue.

    In any case, uses of rvalue-reference-to-function are quite rare and obscure, usually lvalue reference is enough for most needs (although here is one example when rvalue reference is needed).