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

前端 未结 5 699
时光取名叫无心
时光取名叫无心 2021-01-03 21:39

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 

        
相关标签:
5条回答
  • 2021-01-03 21:48

    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.

    0 讨论(0)
  • 2021-01-03 21:49

    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?

    0 讨论(0)
  • 2021-01-03 22:03

    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

    0 讨论(0)
  • 2021-01-03 22:10

    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.

    0 讨论(0)
  • 2021-01-03 22:12

    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.

    0 讨论(0)
提交回复
热议问题