Search code examples
c++templatestype-deductionreference-collapsing

Type deduction and reference collapsing in CTAD


Seems like I don't understand something fundamental about the type deduction /reference collapsing rules of C++. Say I have an object entity which takes by rvalue reference in the constructor and has a data member of the same type. I was fine with this until I learned that the deduced type is deduced according to reference collapsing rules, e.g.:

When I bind an xvalue Alloc&& to the parameter Alloc&& alloc, the deduced type of Alloc will be Alloc&& according to:

  1. A& & becomes A&
  2. A& && becomes A&
  3. A&& & becomes A&
  4. A&& && becomes A&&

So when the deduced type "Alloc" is actually Alloc&& in the following example, why is it that this class seems to store the value type Alloc rathern than the deduced rvalue reference? Shouldn't the class member type "Alloc" secretly be an rvalue reference, since im calling the ctor with an xvalue (std::move)?

Demo

#include <memory>
#include <cstdio>
#include <type_traits>

template <typename Alloc>
struct entity
{
    entity(Alloc&& alloc)
        :   alloc_{ alloc }
    {}

    auto print() {
        if constexpr (std::is_rvalue_reference_v<Alloc>) {
            printf("Is type is &&");
        } else if constexpr (std::is_lvalue_reference_v<Alloc>) {
            printf("Is type is &");
        } else {
            printf("Is type value");
        }
    }

    Alloc alloc_;
};


int main()
{
    std::allocator<int> a;
    entity e(std::move(a));
    e.print();
}

Output:

Is type value

Solution

  • Alloc&& isn't a forwarding reference as it's not used in a function template, it's just an rvalue reference. Therefore Alloc is deduced to be std::allocator<int> and alloc in your constructor is an rvalue reference.

    To see forwarding references you need a function template. E.g.:

    #include <cstdio>
    #include <memory>
    
    template <typename Alloc>
    void print(Alloc&& alloc)
    {
        if constexpr (std::is_rvalue_reference_v<Alloc>) {
            printf("Is type is &&\n");
        } else if constexpr (std::is_lvalue_reference_v<Alloc>) {
            printf("Is type is &\n");
        } else {
            printf("Is type value\n");
        }
    }
    
    int main()
    {
        print(std::allocator<int>());
        std::allocator<int> a;
        print(a);
        print(std::move(a));
    }
    

    Note however that Alloc will still not be an rvalue reference, when passed an rvalue Alloc deduces to std::allocator but alloc is an rvalue reference:

    #include <cstdio>
    #include <memory>
    
    template <typename Alloc>
    void print(Alloc&& alloc)
    {
        if constexpr (std::is_rvalue_reference_v<Alloc>) {
            printf("Is type is &&\n");
        } else if constexpr (std::is_lvalue_reference_v<Alloc>) {
            printf("Is type is &\n");
        } else {
            printf("Is type value\n");
        }
        if constexpr (std::is_rvalue_reference_v<decltype(alloc)>) {
            printf("Argument is type is &&\n");
        } else if constexpr (std::is_lvalue_reference_v<decltype(alloc)>) {
            printf("Argument is type is &\n");
        } else {
            printf("Argument is type value\n");
        }
    }
    
    int main()
    {
        print(std::allocator<int>());
        std::allocator<int> a;
        print(a);
        print(std::move(a));
    }
    

    Prints:

    Is type value
    Argument is type is &&
    Is type is &
    Argument is type is &
    Is type value
    Argument is type is &&