Search code examples
c++rvalue-referenceif-constexpr

Function that can receive any of T, T&, and T&& as input, AND also recognize its type?


I want to create a function that correctly recognizes the type of its parameter.

template<class T> void test(T&& t){
    if constexpr(std::is_same_v<T,int>){
        std::cout<< "= int"<<std::endl;
    }
    if constexpr(std::is_same_v<T,const int&>){
        std::cout<< "= const int&"<<std::endl;
    }
    if constexpr(std::is_same_v<T,int&>){
        std::cout<< "= int&"<<std::endl;
    }
    if constexpr(std::is_same_v<T,int&&>){
        std::cout<< "= int&&"<<std::endl;
    }
    if constexpr(std::is_same_v<T,std::nullptr_t>){
        std::cout<< "= std::nullptr_t"<<std::endl;
    }
}

int funcReturnInt(){return 5;}
    
int main(){
    const int a=0;
    test(funcReturnInt()); // int  (correct)
    test(a);        // const int&   (correct)
    test(nullptr);  // std::nullptr_t  (correct)
    int s=0;
    test(std::move(s)); // print "int" (not "int&&") , I am sad
}

How to create the test() function so it can recognize the type of its single parameter correctly in every case?

I am new to T&&, so I have read some guides about rvalue references (1, 2, 3), but they don't describe how to create such a full-awareness wildcard function like test<T>(T??).

I will apply the solution as test() requires ... {} later.

I prefer to avoid overloading, because I love to make the code compact in 1 place (for educational purpose, too).

Edit :: Very useful comments state that it is generally not possible. Why does my code check other cases correctly, but C++ takes a liberty to get rid of my &&? Please post it as a solution.

I still doubt why int& in main() becomes T=int& correctly but int&& in main() becomes T=int.


Solution

  • Emphasis mine:

    I am new to T&&, so I have read some guides about rvalue references (1, 2, 3), but they don't describe how to create such a full-awareness wildcard function like test<T>(T??).

    In this declaration:

    template<class T> void test(T&& t)
    

    whilst commonly mistaken for one, t is not an rvalue reference, it is forwarding reference (sometimes referred to as universal references). To understand forwarding references we need to understand more than rvalue references, e.g. reference collapsing rules and perfect forwarding. See e.g. the following Q&A for details:

    I still doubt why int& in main() becomes T=int& correctly but int&& in main() becomes `T=int.

    In the call

    test(std::move(s));
    

    the argument type A is int&&, the complete parameter type is int&& meaning the deduced T is int

    test<int>(int&& t);
         ^^^  ^^^^^ parameter-type (T&& -> int&&)
           \ deduced T
    

    Refer to e.g. [temp.deduct.call]/3 for details.

    Universal references particularly apply reference collapsing rules when the argument type is an lvalue. For the call test(a) the argument type is int const, but for purposes of deduction, int const& is used in place, deducing T to int const&, where the resulting function parameter type T&& is collapsed from int const& && to int const& (&& & -> &).