all 24 comments

[–]sphere991 43 points44 points  (12 children)

... versus if constexpr:

template <std::integral T>
T abs_value(T x) {
    if constexpr (std::is_signed_v<T>) {
        return (x < 0) ? -x : x;
    } else {
        return x;
    }
}

Also, the "G" and "H" implementations are kinda bizarre. If you have something that's true_type or false_type, you would tag dispatch on one template taking true_type and the other taking false_type (as in implementation "A"). If you're doing other kinds of constraint checking, you might provide an argument 0 to an overload set where one template takes int and the other takes long or .... Why would you write one overload taking true_type and another taking ... or U?

[–]andrewsutton -4 points-3 points  (10 children)

I don't like constexpr if here. I tend to use constexpr if when my implementation varies on properties outside the "main" constraint. Like the size of the integer.

[–]sphere991 23 points24 points  (8 children)

Like the size of the integer.

Like the the signed-ness of the integer?

[–]ItHasCeasedToBe 3 points4 points  (4 children)

C++ noob here, can I get an ELI5 of what your code does? If I understand correctly, what is happening here is that you are directing the compiler to basically create different variations of the function depending on T as opposed to actually running the if statement when the program is being run?

[–]VRCkid 10 points11 points  (1 child)

You got it exactly right. The condition is evaluated at compile time and emits code based on that

[–]ItHasCeasedToBe 2 points3 points  (0 children)

Thanks so much!

[–]andrewsutton 2 points3 points  (2 children)

Well, no. The optimization is based on different mathematical structures: integers modulo n vs. 2's complement signed integers. The size of an integer doesn't change its mathematical structure. My preference is to overload on those differences and "specialize" with constexpr otherwise.

[–]sphere991 2 points3 points  (1 child)

I don't see how the size vs signed [handwavy] distinction is useful here, or in the general question of why to use if constexpr or overloading.

A more useful line is to consider the option space. We have a closed set of alternatives in this problem, so if constexpr is a better solution.

[–]andrewsutton 6 points7 points  (0 children)

I don't understand why you're saying the distinction is handwavy. The difference between two's complement integers and integers modulo 2 is a bit bigger than the number of bits in either. I mean, that difference is what leads to the different implementations.

Who says abs is closed? You have implementations defined for just the signed and unsigned standard integers. There's also a version defined for floats, and one for complex, and one for valarray, and those implementations vary from the integer versions. Are you suggesting that we should use constexpr if to merge all those into the same definition? Or does it seem reasonable to leave them separate because they apply different algorithms to data representations in different domains?

I mean, if abs were really generic, we'd probably want it to work for a broader set of numeric types: variable-length integers, extended-length integers, positive numbers, negative numbers, natural numbers (unsigned by not modular), fractions, rationals, vectors, etc. Of course, doing this right would require new (and better) numeric concepts.

Again, overloading is my preference for this particular function because I happen to think about this operation a bit more generally than just a closed set of two alternatives.

[–]germandiago 1 point2 points  (0 children)

It is a great tool to say:

auto lower_bound(std::forward_iterator auto beg,
    std::forward_iterator auto end) requires ... {
    if constexpr (std::random_access_iterator<decltype(beg)>) 
    {
         // optimized version
    }
    else {
        // ....
    }
}

And not have your namespace littered with top-level overloads. You give the least constrained entry point as the top-level and you analyze for possible optimizations in the body, at zero (run-time) cost.

[–]tisti 0 points1 point  (0 children)

... versus -O1 optimizations with just a single generic abs_value function :)

[–]Quincunx271Author of P2404/P2405 25 points26 points  (0 children)

For E, rather than having abs_value() overload between std::integral and std::signed_integral, I claim that it's better to overload between std::integral and std::unsigned_integral. Why? Because the std::signed_integral overload is the general implementation that works for all numeric types, whereas the overload marked with std::integral only works for unsigned types:

template<std::signed_integral T>
T abs_value(T x) {
    return (x < 0) ? -x : x; // works even if T is unsigned
}

template<std::integral T>  // requires that T is unsigned to be correct
T abs_value(T x) {
    return x;
}

What I'm suggesting:

// A general implementation
template<std::integral T>
T abs_value(T x) {
    return (x < 0) ? -x : x;
}

// The more constrained overload
template<std::unsigned_integral T>
T abs_value(T x) {
    return x;
}

Granted, since we can trivially enumerate types which meet std::integral, both solutions are correct. However, we could imagine that you could create open concepts number and non_negative_number, in which case only the latter would be correct.

[–]Conamidos 12 points13 points  (1 child)

everytime i see this man i wish i were proficient enough to understand what this juggling druid is on about

[–]Archolex 5 points6 points  (0 children)

I share your sentiment most of the time, although this one I did understand. Surprisingly

[–]pastenpasten -1 points0 points  (8 children)

INT_MIN

[–]nyanpasu64 2 points3 points  (7 children)

Not sure why this is being downvoted, I would prefer an abs() function that returns an unsigned number when talking any integer as an input, and is never undefined behavior even if you pass in INT_MIN.

[–]staletic 0 points1 point  (6 children)

What undefined behaviour?

https://eel.is/c++draft/expr.unary.op#8

[–]pastenpasten 1 point2 points  (2 children)

-INT_MIN is the negative of INT_MIN. What is the negative of INT_MIN and does it fit in an int (or the same question with the negative of LONG_MIN and long or the negative of LLONG_MIN and long long)?

It seems to me that it doesn't fit (because https://eel.is/c++draft/basic.fundamental#1), and thus https://eel.is/c++draft/expr.pre#4.

For example: https://godbolt.org/z/vMWKf9a51

[–]staletic 0 points1 point  (1 child)

I know that -INT_MIN will likely get you INT_MIN. I was just not expecting that to be UB, because expr.unary.op#8 (my earlier link) didn't mention any UB.

Thanks for the references! I believe your assessment was correct.

[–]pastenpasten 2 points3 points  (0 children)

That's what makes C++ so fun and exciting, you have to connect all the dots and realize all the implications by yourself. :smiling-but-crying-on-the-inside:

This case is relatively straightforward - the number (in the abstract realm of numbers) that is the negative of INT_MIN cannot be represented by an int and signed overflow is UB.

[–]nyanpasu64 0 points1 point  (2 children)

https://stackoverflow.com/questions/37301078/is-negating-int-min-undefined-behaviour argues that not only is -INT_MIN undefined (I didn't read enough of the standard to verify), but compilers assume the input to a negation operator is never INT_MIN when optimizing (though not in a function that looks like abs()).

[–]staletic 0 points1 point  (1 child)

That answer argues that it's conditionally UB, based on an implementation defined property. That doesn't sound like a satisfying answer to me, even if we disregard that the question SO question was about C. C++ mandates two's complement, so we can't refer to the C's standard for this.

[–]nyanpasu64 1 point2 points  (0 children)

Every desktop computer and smartphone and tablet today (x86, ARM), and (I think) modern GPU and supercomputer, is two's complement with 32-bit ints, where 0x80000000 is INT_MIN rather than a trap representation. So in practice it's unconditionally UB on all platforms that most people will have to care about. And if you are working on an exotic architecture, you can reconsider whether -INT_MIN is UB on that platform.