Why does this function return an lvalue reference given rvalue arguments?
Asked Answered
P

1

13

The following definition of a min function

template <typename T, typename U>
constexpr auto
min(T&& t, U&& u) -> decltype(t < u ? t : u)
{
    return t < u ? t : u;
}

has a problem: it seems that it's perfectly legal to write

min(10, 20) = 0;

This has been tested with Clang 3.5 and g++ 4.9.

The solution is straightforward, just use std::forward to restore the "rvalue-ness" of the arguments, i.e. modify the body and the decltype to say

t < u ? std::forward<T>(t) : std::forward<U>(u)

However, I'm at a loss to explain why the first definition doesn't generate an error.


Given my understanding of forwarding and universal references, both t and u deduce their argument types as int&& when passed integer literals. However, within the body of min, the arguments have names, so they are lvalues. Now, the really complicated rules for the conditional operator come into play, but I think the pertinent line is:

  • Both E2 [and] E3 are glvalues of the same type. In this case, the result has the same type and value category.

and therefore the return type of operator?: should be int&& as well, should it not? However, (as far as I can tell) both Clang and g++ have min(int&&, int&&) returning an lvalue reference int&, thus allowing me to assign to the result.

Clearly there is a gap in my understanding, but I'm not sure exactly what it is that I'm missing. Can anybody explain to me exactly what's going on here?


EDIT:

As Niall correctly points out, the problem here is not with the conditional operator (which is returning an lvalue of type int&& as expected), but with the decltype. The rules for decltype say

if the value category of expression is lvalue, then the decltype specifies T&

so the return value of the function becomes int&& &, which by the reference collapsing rules of C++11 turns into plain int& (contrary to my expected int&&).

But if we use std::forward, we turn the second and third arguments of operator?: (back) into rvalues - specifically, xvalues. Since xvalues are still glvalues (are you keeping up at the back?), the same conditional operator rule applies, and we get a result of the same type and value category: that is, an int&& which is an xvalue.

Now, when the function returns, it triggers a different decltype rule:

if the value category of expression is xvalue, then the decltype specifies T&&

This time, the reference collapsing gives us int&& && = int&& and, more importantly, the function returns an xvalue. This makes it illegal to assign to the return value, just as we'd like.

Pimbley answered 14/10, 2014 at 7:21 Comment(5)
In the body, I think, you may use std::move to tell the compiler explicitly that you don't need these values any more.Favorable
@Favorable As noted, I know the solution (which is to use std::forward rather than std::move, so the function still works with lvalue args). I'm curious as to why the first (incorrect) definition does what it does.Pimbley
The issue may be with the decltype() rules if the trailing return type is removed (i.e. auto min(...) { ... }) and allow the compiler to deduce it, it returns int.Yoghurt
@Yoghurt I was just editing the question to ask something just like that! Thanks :-)Pimbley
The type of the conditional expression isn't an rvalue ideone.com/ufbjIQFlorindaflorine
Y
7

The issue may be with the decltype() rules.

This is hinted at; if the trailing return type is removed

template <typename T, typename U>
constexpr auto
min(T&& t, U&& u)
{
    return t < u ? t : u;
}

and the compiler is allowed to deduce it, the return type is int.

Use of decltype

decltype ( expression )
...
if the value category of expression is lvalue, then the decltype specifies T&

Taken from cppreference.

Since the expression involving t and u is an lvalue (they are lvalues - named r-value references), the return is the lvalue reference.

In this situation it leads to a potential situation where a literal could be modified. Careful use of forwarding needs to be applied when using "universal references" (or "forwarding references") and the associated reference collapsing rules.

As you have already noted, to correct the situation, the correct use of std::forward needs to be applied and the return type will be the expected one.


For further details on std::forward and reference collapsing can be found here on SO.

Yoghurt answered 14/10, 2014 at 8:24 Comment(2)
The plain auto return case is a bit misleading, because auto never deduces a reference (which is why decltype(auto) exists in C++14). But the bit about decltype forming a reference when given an lvalue argument is exactly the answer I was looking for, thanks :-)Pimbley
True, a lot more happens with the auto, it was merely the hint to where the issue could be.Yoghurt

© 2022 - 2024 — McMap. All rights reserved.