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.
std::forward
rather thanstd::move
, so the function still works with lvalue args). I'm curious as to why the first (incorrect) definition does what it does. – Pimbleydecltype()
rules if the trailing return type is removed (i.e.auto min(...) { ... }
) and allow the compiler to deduce it, it returnsint
. – Yoghurt