0

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
glades
  • 3,778
  • 1
  • 12
  • 34
  • This is two questions rolled into one: implicitly generated CTAD and forwarding references. It seems to me you are confused about both. Please ask _one_ at a time. – Passer By Feb 14 '23 at 07:58
  • 1
    `Alloc&&` isn't a [forwarding reference](https://en.cppreference.com/w/cpp/language/reference#Forwarding_references) as it's not used in a function template, it's just an rvalue reference – Alan Birtles Feb 14 '23 at 07:59
  • 2
    Reference collapsing happens after template parameter deduction. They are designed to work well together, but are ultimately separate. *"the deduced type of `Alloc` will be `Alloc&&`"* What? – HolyBlackCat Feb 14 '23 at 08:04
  • @HolyBlackCat That I don't understand. I thought types are deduced *such that* they match the expected type including references if that is possible. I didn't know that rules are different for CTAD and function templates. Any literature on that topic? – glades Feb 14 '23 at 08:22
  • C++ move semantics by Josuttis or his talks from cppcon. The posted code presents sth Josuttis calls "templated entity" but not a function template. You can to sth like [this](https://godbolt.org/z/73x3McGqY) but note a. templated constructor b. manually supplied deduction guide. – alagner Feb 14 '23 at 08:34
  • *"types are deduced such that they match ..."* Yes, but if you look at the collapsing rules alone, you'll have ambiguities. In your case `Alloc = T` and `Alloc = T &&` would both result in the correct parameter type. So the deduction rules have separate wording that cleans this up. | The rules for implicit CTAD are only slightly different, it won't deduce `Alloc = T &` in this case. *"Any literature on that topic?"* Just cppreference, but not sure if it'll be enough. – HolyBlackCat Feb 14 '23 at 09:17
  • @HolyBlackCat Thanks, I guess what I'm looking for is kind of a step by step guide on how the compiler does what he does in this regard. That would be really helpful. But I keep looking :) – glades Feb 14 '23 at 11:06

1 Answers1

2

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 &&
Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
  • `Alloc` is not going to be a reference in your function with `std::move(a)`. The implicitly generated CTAD will behave in precisely the same way as your function. – Passer By Feb 14 '23 at 08:18
  • @PasserBy fixed, knew I shouldn't have written that on my phone without testing first – Alan Birtles Feb 14 '23 at 08:31
  • Thank you that clears things up a bit. Still kind of confused though, but I guess I have to read some literature now... :) – glades Feb 14 '23 at 08:43