Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++: const not allowed in universal (forwarding) reference

Tags:

I was reading this stackoverflow answer where one reason why const T&& is not a universal (forwarding) reference is given:

Allowing const T&& to behave as forwarding references, would make it impossible to overload a template function who takes only an rvalue reference as parameter.

I do not know what this means. I guess it implies having two overloads of the same function (template), and one of them taking as a parameter const T&&. I also assume one of these overloads will always be called, while the other will never be called.

If my assumption is correct, what are the two overloaded functions? Or if I am wrong, what does the quoted paragraph actually mean?

Thank you.

like image 941
user42768 Avatar asked Jul 02 '18 21:07

user42768


1 Answers

As far as I can see the section of answer that you have quoted is accurate but misleading.

First it's important to clarify that an rvalue reference and a forwarding reference are not the same thing, they just share the same notation &&. Whether this is a good thing is up for debate.

template <typename T>
void foo(T&&); // deduced type == forwarding reference

void foo(int&&); // explicit type == rvalue reference

Simple enough. So why isn't the following a forwarding reference?

template <typename T>
void foo(const T&&); // const rvalue reference despite deduced type

The best answer I can give you is 'because'. It appears to be a completely arbitrary decision by the Standards Committee. I can see no reason why const T&& can't be a a forwarding reference; it just isn't because the standard says so.

§14.8.2.1/ Deducing template arguments from a function call [temp.deduct.call]

A forwarding reference is an rvalue reference to a cv-unqualified template parameter.

Regardless of why this is the case, it becomes apparent that adding cv-qualification is the only way to tell the compiler to treat a deduced type as an rvalue reference rather than a forwarding reference. Which is the point made by your quote from the other answer.

Allowing const T&& to behave as forwarding references, would make it impossible to overload a template function who takes only an rvalue reference as parameter.

The reason I say this is misleading is because it implies that if we overload a template to accept const T&& then this overload will be preferred for all rvalue references regardless of cv-qualification. This is not the case.

In the following code we can see that bar accepts const rvalue references but nothing else because val is not a forwarding reference.

struct Non_POD
{
    Non_POD(int i) : m_i(i) { }
    int m_i;
};

Non_POD foo() { return {0}; }

const Non_POD const_foo() { return {0}; }

template <typename T>
void bar(const T&& val)
{
    std::cout << "Accepts: const rvalue ref. ";
    if constexpr (std::is_rvalue_reference_v<decltype(val)>)
    {
        std::cout << "Val is rvalue reference.\n";
    }
    else if constexpr (std::is_lvalue_reference_v<decltype(val)>)
    {
        std::cout << "Val is lvalue reference.\n";
    }
    else
    {
        std::cout << "Val is lvalue.\n";
    }

    std::cout << std::endl;
}

int main()
{
    bar(foo());
    bar(const_foo());
    Non_POD x(0);
    //bar(x); // error
}

Expected Output (GCC 7.1)

Accepts: const rvalue ref. Val is rvalue reference.

Accepts: const rvalue ref. Val is rvalue reference.

This seems to support the quote, because bar accepts const rvalue references and converts rvalue references into const rvalue references. However there's no overloading going on. If we introduce overloading we can see that bar only accepts const rvalue references.

struct Non_POD
{
    Non_POD(int i) : m_i(i) { }
    int m_i;
};

Non_POD foo() { return {0}; }

const Non_POD const_foo() { return {0}; }

template <typename T>
void bar(const T&& val)
{
    std::cout << "Accepts: const rvalue ref. ";
    if constexpr (std::is_rvalue_reference_v<decltype(val)>)
    {
        std::cout << "Val is rvalue reference.\n";
    }
    else if constexpr (std::is_lvalue_reference_v<decltype(val)>)
    {
        std::cout << "Val is lvalue reference.\n";
    }
    else
    {
        std::cout << "Val is lvalue.\n";
    }

    std::cout << std::endl;
}

template <typename T>
void bar(T&& val)
{
    std::cout << "Accepts: forwarding ref. ";
    if constexpr (std::is_rvalue_reference_v<decltype(val)>)
    {
        std::cout << "Val is rvalue reference.\n";
    }
    else if constexpr (std::is_lvalue_reference_v<decltype(val)>)
    {
        std::cout << "Val is lvalue reference.\n";
    }
    else
    {
        std::cout << "Val is lvalue.\n";
    }

    std::cout << std::endl;
}

int main()
{
    Non_POD x(0);
    const Non_POD cx(0);

    bar(x);
    bar(cx);
    bar(Non_POD(0));
    bar(foo());
    bar(const_foo());
}

Expected Output (GCC 7.1)

Accepts: forwarding ref. Val is lvalue reference.

Accepts: forwarding ref. Val is lvalue reference.

Accepts: forwarding ref. Val is rvalue reference.

Accepts: forwarding ref. Val is rvalue reference.

Accepts: const rvalue ref. Val is rvalue reference.

We can see from the above that there is actually no way to declare a template that only accepts non const rvalue references.

like image 122
Fibbs Avatar answered Sep 28 '22 17:09

Fibbs