Search code examples
c++templatesforwarding-reference

Whether `const` will be discarded or not when deducing from universal reference?


I found this question when learning the universal(forwarding) reference. The code is as follows.

#include <iostream> // std::cout

#include <type_traits>

template <class T> // deductions among universal reference
void fun(T &&b)    // fold reference, maintain const
{
    std::cout << b << " ";
    std::cout << std::is_const<T>::value << " ";
    std::cout << std::is_lvalue_reference<T>::value << " ";
    std::cout << std::is_rvalue_reference<T>::value << std::endl;
}

int main()
{
    int a = 1;
    fun(a);            // lvalue: T=int&
    fun(std::move(a)); // rvalue: T=int

    const int ca = 1;
    fun(ca);            // const_lvalue: T=int& (why no const?)
    fun(std::move(ca)); // const_rvalue: T=const int

    int &la = a;
    fun(la); // lvalue_ref: T=int&

    int &&ra = 1;
    fun(ra);                             // rvalue_ref: T=int& (r_ref is an l_val)
    fun(std::forward<decltype(ra)>(ra)); // rvalue_ref + perfect forwarding T=int

    const int &cla = a;
    fun(cla); // const_lvalue_ref: T=int& (no const?)

    const int &&cra = 1;
    fun(cra); // const_rvalue_ref: T=int& (no const?)

    return 0;
}

The results are as follows. As can be seen, when the input argument is lvalue, const is discarded when parsing the type of T. However, when it is r_value, const is maintained.

1 0 1 0
1 0 0 0
1 0 1 0  <-const is discarded   //const_lvalue: T=int&
1 1 0 0  <-const is maintained  //const_rvalue: T=const int
1 0 1 0
1 0 1 0
1 0 0 0
1 0 1 0  <-const is discarded  //const_lvalue_ref: T=int&
1 0 1 0  <-const is discarded  //const_rvalue_ref: T=int&

Moreover, when I tried to use cppinsights to run this code, it generates 4 template specializations.

/* First instantiated from: insights.cpp:18 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void fun<int &>(int & b)
{

}
#endif

/* First instantiated from: insights.cpp:19 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void fun<int>(int && b)
{

}
#endif

/* First instantiated from: insights.cpp:22 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void fun<const int &>(const int & b)
{

}
#endif


/* First instantiated from: insights.cpp:23 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void fun<const int>(const int && b)
{

}
#endif

As can be seen from the result, const is maintained after the parsing process in cppinsights.

Can someone tell me why the results from my code and cppinsights are different? (Why l_value in my code is deduced to T& rather than const T&?)

In other words, what is the correct answer for the problem: Whether const will be discarded or not when deducing from universal reference?


Solution

  • const is never implicitly discarded, your code for testing whether something "is const" using std::is_const is just flawed. std::is_const does not consider the constness of a referred type:

    std::is_const_v<const int> // true
    std::is_const_v<const int&> // false, a const& is not a const type,
                                // it refers to something const
    

    Consider what happens in your code:

    const int ca = 1;
    fun(ca); // calls fun<const int&>(ca);
    
    // then:
    template <class T> // T = const int&
    void fun(T &&b)    // b is declared as const int&
    {
        // ...
        std::cout << std::is_const<T>::value << " "; // prints 0, same reason as above
        // ...
    

    When bound to lvalues of type const L, a forwarding reference T&& will deduce T to const L&. After reference collapsing, T&& is just const L&.

    For your code to give you the expected result, you must use:

    std::is_const<typename std::remove_reference<T>::type>::value
    // or since C++14
    std::is_const<std::remove_reference_t<T>>::value
    // or since C++17
    std::is_const_v<std::remove_reference_t<T>>