Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is this "min" template of cpp-next at fault?

I was reading cpp-next where this min template is presented as an example of how verbose C++ code can be compared to python code

template <class T, class U>
auto min(T x, U y)->decltype(x < y ? x : y)
{ return x < y ? x : y; }

At first this looks innocent but Daveed Vandevoorde made this remark

The min template that uses decltype in its return type specification doesn’t work: It returns a reference (because the argument is an lvalue) that ends up referring to a local variable in most common uses.

I figured it may not be clear to everyone how the problem manifests. Can you please give a detailed explanation and possible fixes?

like image 253
Johannes Schaub - litb Avatar asked Nov 19 '11 15:11

Johannes Schaub - litb


5 Answers

The problem is that the arguments aren't taken as references. This invokes slicing, in the case of polymorphic types, and then a reference return to local variable. The solution is to take the arguments as rvalue references, invoking perfect forwarding, and then simply deduce and return the return type. When this is done, returning a reference is just fine, as the value still exists.

like image 143
Puppy Avatar answered Sep 29 '22 20:09

Puppy


rev 3: KonradRudolph

template <class T, class U>
auto min(T x, U y) -> typename std::remove_reference<decltype(x < y ? x : y)>::type
{ 
    return x < y ? x : y; 
}

rev 2: KennyTM

template <class T, class U>
auto min(T x, U y)->decltype(x < y ? std::declval<T>() : std::declval<U>())
{ 
    return x < y ? x : y; 
}

rev 1: T and U must be default constructible

template <class T, class U>
auto min(T x, U y)->decltype(x < y ? T() : U())
{ 
    return x < y ? x : y; 
}

test:

int main()
{
   int x; int y;
   static_assert(std::is_same<decltype(min(x, y)), int>::value, "");
   return 0;
}

EDIT:

I'm a bit surprised but it actually compiles with remove_reference.

like image 40
ronag Avatar answered Sep 29 '22 20:09

ronag


The arguments are passed by value (T and U deduced as int), but the type of ?: expression is deduced as a reference in this case since they are local lvalues inside the function. Specifics will be in @Johannes' answer that should come in a few minutes. :D

like image 26
Xeo Avatar answered Sep 29 '22 22:09

Xeo


What's all the fuss, and why isn't anyone trying the obvious solution, which is perfect forwarding?

template <class T, class U>
typename std::enable_if< ! std::is_integral< T >() || ! std::is_integral< U >(),
                         typename std::common_type< T, U >::type >::type
min(T &&x, U &&y)
    { return x < y ? std::forward< T >( x ) : std::forward< U >( y ); }

template <class T, class U>
decltype( typename std::enable_if< std::is_integral< T >() && std::is_integral< U >(),
                         decltype( typename std::common_type< T, U >
         ::type{ U( -1 ) } ) >::type{ T( -1 ) } )
min(T &&x, U &&y)
    { return x < y ? std::forward< T >( x ) : std::forward< U >( y ); }

Now it works just as if you put the expression in the calling function, which is exactly what the user expects (and simply the best thing overall).

Edit: Now it prohibits dangerous unsigned vs. signed operations, per Howard's paper, by requiring that the conversion from each operand type to the result type be non-narrowing if both operands are of integral type. However, GCC won't compile this, complaining "sorry, unimplemented: mangling constructor." This seems to occur if uniform initialization is used in any way in the function signature.

like image 41
Potatoswatter Avatar answered Sep 29 '22 21:09

Potatoswatter


Returning by reference might sometimes be a feature, not a bug. We'll return to this later. First a recap of the basics:

int x; int y;
x    // this is an lvalue
y    // lvalue also
x+y  // not an lvalue - you couldn't do (x+y) = 3
x<y?x:y // lvalue - you can do (x<y?x:y) = 0

The last line shows that a ?: can often be an lvalue. i.e. You can do (x<y?x:y)=0 to set the smallest variable to 0 and leave the other one alone. Of course, you can't do (1<3?6:8)=0 as you can't do 6=0 or 8=0. So it's just an rvalue in that case.

Inside min, x and y are the names of the function parameters and hence are lvalues. decltype(x<y?:x:y) is int&. (I found this other cpp-Next article useful also.)

So why might this be a problem? Well, if the return type of min is a reference, then it will return a reference to one of x or y, the function parameters. The question now is, were x and y references themselves?

Consider this use case:

int m = 5; int n = 10;
min(m,n) = 0; // do you want this to work?

We have a decision to make. Maybe we want min to return references, if the arguments to min were references. I guess it's somewhat a matter of taste. If you rigorous want to return only non-references, this is easy to enforce with std::remove_reference around the decltype(x<y?x:y). But that's boring. Let's allow ourselves to (sometimes) return references; it might be more efficient and useful in many cases.

If you use the original example definition of min, along with non-reference types for x or y, then min will return a reference to the local values among its parameters. This is bad as the references will be invalid and the behaviour undefined. For example, this would be bad:

int p = min(5,8); // reading from a now-invalid reference.

So, we have to go through a variety of use-cases and decide what behaviour we want:

// Desired behaviour
int m = 5;
int n = 10;
min(3,7); // return by value. i.e. return an int
min(m,n); // return an int& which maps to either m or n
min(3,n); // return by value
min(foo(), bar()) // what makes sense here?

Can we all agree on what behaviour we would want from such a min? And then, how do we implement it?

like image 23
Aaron McDaid Avatar answered Sep 29 '22 20:09

Aaron McDaid