2

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.

cppBeginner
  • 1,114
  • 9
  • 27
  • 1
    What is the underlying problem you need to solve? Why do you need to "correctly recognize type of parameter"? How will that solve the underlying problem? If you ask about the actual, real and underlying problem directly, perhaps we might be able to help you better? Also please read about [the XY problem](https://en.wikipedia.org/wiki/XY_problem), because at the moment your question is one. – Some programmer dude Aug 10 '23 at 07:13
  • @Someprogrammerdude :: Sorry, I stray so far from the underlying problem. From the code in my question, I already got everything I want to fix my real problem. However, after playing with it too much, I can't restrain my curiosity about the possibility of existence of such `test()` function. Perhaps, with this technique, I can group move/copy function into a single function, or do other fun tricks. – cppBeginner Aug 10 '23 at 07:16
  • 2
    I general, you can't detect the difference between `T &&` and `T` without something like macros, which would use `decltype` internally. – HolyBlackCat Aug 10 '23 at 07:18
  • 2
    @cppBeginner it's not possible without designing it for each type. But depending on use case, there are multiple patterns of *dispatching*. Overloading and type erasure with a visitor are most common ones. – Swift - Friday Pie Aug 10 '23 at 07:18
  • @HolyBlackCat :: Thank! It is great to know that there is no cool way to do it. May you provide a reference please? I want to read more about it. – cppBeginner Aug 10 '23 at 07:19
  • @Swift-FridayPie :: It is very useful! – cppBeginner Aug 10 '23 at 07:20
  • 3
    It's hard to prove a negative. The standard and cppreference usually list the things that are possible, they don't explicitly say that such-and-such is impossible. – HolyBlackCat Aug 10 '23 at 07:25
  • If you want to recognize the type of the function parameter `t`, why do you check what `T` is? You can check `decltype(t)` instead. Note that the type of `t` will never be plain `int`. It will always be a reference of some kind. Live demo: https://godbolt.org/z/8dcn6hxGc. – Daniel Langr Aug 10 '23 at 07:37
  • 1
    @DanielLangr Both `test(42);` and `test(std::move(s))` will print `int &&` with your code, but the first one is a prvalue and the second one is an xvalue. I'm telling OP you can't detect the difference with templates alone. – HolyBlackCat Aug 10 '23 at 07:38
  • @HolyBlackCat I agree, but I thought OP cared about the type of the function parameter (i.e., `t`). Those value categories refer to function arguments instead. Maybe I didn't understand the question correctly. – Daniel Langr Aug 10 '23 at 07:40
  • @DanielLangr :: It will print weird results with `&&` fever https://wandbox.org/permlink/ueokPZwxMX54DF7C – cppBeginner Aug 10 '23 at 07:41
  • 1
    It seems the type of `std::move(int)` is `int` not `int&&` : https://godbolt.org/z/7G6zfb11c – Pepijn Kramer Aug 10 '23 at 07:41
  • @PepijnKramer :: I think `auto` in your example is wrong here. While type of `auto x=std::move(int)` is `int`, type of `std::move(int)` is actually `int&&`. https://godbolt.org/z/5YzGbv6hv ... I am not sure why C++ did this. It is a fun discovery, thanks. – cppBeginner Aug 10 '23 at 07:47
  • @cppBeginner I wouldn't say that `auto` is "wrong". It just works such that it does not produce references automatically: [C++11 "auto" semantics](https://stackoverflow.com/q/8542873/580083). Anyway, this question might be related to your problem: [Empirically determine value category of C++11 expression?](https://stackoverflow.com/q/16637945/580083). – Daniel Langr Aug 10 '23 at 07:52
  • @DanielLangr O yes I forgot about that behavior of auto. WIth that corrected : https://godbolt.org/z/zYhhj9rvq. decltype(std::move(int)) is indeed int&& – Pepijn Kramer Aug 10 '23 at 07:54
  • 1
    `T&&` is not an rvalue reference, it is a forwarding (/"universal") reference, as such you may want to expand your reading material beyond rvalue references to also include reference collapsing rules and perfect forwarding. – dfrib Aug 10 '23 at 07:57
  • @DanielLangr :: Oh, `auto` is so tricky. Your second link is an interesting workaround using MACRO. Now, I only want the reason why it is impossible. – cppBeginner Aug 10 '23 at 07:59
  • @dfrib :: Sound like the essential knowledge I am lacking. Thank. I will read more about "reference collapsing" in https://en.cppreference.com/w/cpp/language/reference . – cppBeginner Aug 10 '23 at 08:02
  • 1
    @cppBeginner I agree with HolyBlackCat's second comment. The reason simply is that nothing in the standard says that it is possible. How would you want to prove it here? One would need to read the entire standard to do so. – Daniel Langr Aug 10 '23 at 08:04
  • @DanielLangr :: I doubt why `int&` in `main()` becomes `T=int&` correctly but `int&&` in `main()` becomes `T=int`. It makes no sense for me at all. Perhaps, I know too little to express my doubt correctly, sorry. I probably need to read more about it, like dfrib mentioned. – cppBeginner Aug 10 '23 at 08:09
  • 1
    The function takes `T&&` as a parameter. So if you pass in `int&&`, `T` will be `int` and the parameter type will be `int&&`. If you pass in `int &`, both `T` and the parameter type will be `int &` (this is because of reference collapsing: X & && is collapsed to X &). – interjay Aug 10 '23 at 08:35
  • 1
    @cppBeginner This is how template argument deduction works with forwarding references. Basically, if the function argument is an _lvalue_ of type `X`, then `T` (template parameter) is deduced as `X&`. If the argument is an _rvalue_ of type `X`, then `T` is deduced as `X` only. Finally, the deduced template parameter (`X&` or `X`) is substituted for `T` anywhere `T` occurs, and possibly reference collapsing rules apply. Therefore, the type of `t` becomes either `X& &&` or `X&&` in our two cases, which, after collapsing, gives `X&` and `X&&`. – Daniel Langr Aug 10 '23 at 08:42
  • @dfrib :: Researching in such area has helped me a lot. After reading them, I could sleep well now, many thanks. If you don't mind, please post it as a solution. – cppBeginner Aug 15 '23 at 00:58
  • @cppBeginner Happy to help! I added a short answer to summarize for future readers. – dfrib Aug 15 '23 at 12:13

1 Answers1

2

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& (&& & -> &).

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • _"the call `test(std::move(s))` results in rvalue reference to rvalue reference"_. Are you sure this is correct? IIRC, here the template argument is deduced as `int` only, and then substituted for `T`. Therefore, the parameter `t` is an rvalue reference directly without any application of collapsing rules. Live demo: https://godbolt.org/z/M3W8baKan. – Daniel Langr Aug 16 '23 at 06:53
  • 1
    @DanielLangr Your demo yields the same result as my own (faulty) argument (showing that the deduced `T` is indeed `int`), but yes your are correct that reference collapsing rules are not relevant here. For an argument of type `int&&` and forwarding reference `T` as a function parameter `T&&`, the `T` simply deduces to `int`. Whereas it is only for the case where the argument type is an lvalue, say `int&`, that [reference collapsing rules](https://timsong-cpp.github.io/cppwp/n4868/temp.deduct.call#3.sentence-4) applies. Thanks. – dfrib Aug 16 '23 at 10:34