11
\$\begingroup\$

I wanted to write a generic abs function that would correctly work for every type. Basically, I wanted to use the following algorithm:

  • If the type is a built-in integer, use std::abs from <cstdlib>.
  • If the type is a built-in floating-point, use std::abs from <cmath>.
  • If the type is a user-defined type with a namespace-level abs, use it.
  • Otherwise, call a generic abs algorithm.

Here is what I came up with:

#include <cmath>
#include <cstdlib>

namespace math
{
    namespace detail
    {
        // generic abs algorithm
        template<typename T>
        constexpr auto abs(const T& value)
            -> T
        {
            return (T{} < value) ? value : -value;
        }
    }

    template<typename T>
    constexpr auto abs(const T& value)
        -> T
    {
        using std::abs;
        using detail::abs;
        return abs(value);
    }
}

The idea is to create a generic detail::abs algorithm, then to create another abs function that will choose the function to call thanks to the argument-dependant lookup. Here is what I tried to take into account:

  • While the generic algorithm also works for built-in integral and floating point types, std::abs may produce optimized code for these types. Using std::abs when possible will probably generate an optimized executable. That said, std::abs lacks constexpr, which is a desirable feature, and the compiler might recognize a absolute value-like construct and optimize it away...

  • Some types have a namespace-level abs that does not behave like the generic algorithm. Therefore, we have to call this namespace-level function if it exist.

  • Some types may be huge. Therefore, I chose to take the parameter by const& since some namespace-level abs may also take their parameter by const&.

  • Some types only provide operator< to represent the ordering, but not the other relational operators. Therefore, calling operator< in the generic algorithm is more likely to work.

  • I chose to use T{} instead of 0 for the comparison in the generic algorithm in order to be able to represent the default value for any given type. A type is not guaranteed to be comparable to an integer.

Here is a test case to demonstrate what the function can achieve (you can also test it online):

namespace eggs
{
    struct Foo
    {
        Foo(int val):
            val(val)
        {}

        int val;
    };

    Foo abs(Foo foo)
    {
        return { std::abs(foo.val) };
    }
}

struct Bar
{
    Bar(int val=0):
        val(val)
    {}

    Bar operator-() const
    {
        return { -val };
    }

    int val;
};

bool operator<(const Bar& lhs, const Bar& rhs)
{
    return lhs.val < rhs.val;
}

int main()
{
    using namespace std::literals;

    std::cout << math::abs(-5) << '\n';
    std::cout << math::abs(-5.3f) << '\n';
    std::cout << math::abs(-5i+2.0) << '\n';

    eggs::Foo foo = { -8 };
    std::cout << math::abs(foo).val << '\n';

    Bar bar = { -9 };
    std::cout << math::abs(bar).val << '\n';
}

What do you think of such a function? Did I miss any obscure error? Do you see anything that could be improved (in the implementation, I don't care about the test case)?

\$\endgroup\$
4
\$\begingroup\$

I believe you have a bug in your generic implementation, especially in relation to floating-point style classes that have signed-zero values:

your code:

return (T{} < value) ? value : -value;

would be better as a T{} <= value (or rewritten as (T{} > value) ? -value : value;).

Your current logic will return -0.0 for an input value of 0.0, and that's not appropriate for an abs() function.

Additionally, I don't know how you would really test these things, because, if I am not mistaken, in floating-point comparisons with signed-zero values, -0.0 == 0.0 yet I would expect that abs(-0.0) would return 0.0.

How you resolve this issue though, I don't know.

\$\endgroup\$
  • \$\begingroup\$ That's actually a question: if it is not observable, does it matter? \$\endgroup\$ – Morwenn Aug 15 '14 at 16:04
  • 2
    \$\begingroup\$ @Morwenn It's observable. To test, divide by zero! 1.0 / 0.0 is inf. 1.0 / -0.0 is -inf. \$\endgroup\$ – 200_success Aug 15 '14 at 16:42
  • \$\begingroup\$ @200_success You're right. I'm not used to handling floating point issues. I almost never run into problems, so that's something that I generally don't even take into account (and that's a shame). That said, floating point values are handled by std::abs and not by the generic algorithm. Therefore, I don't think that I have a bug. \$\endgroup\$ – Morwenn Aug 15 '14 at 23:53
  • \$\begingroup\$ @Morwenn - you will still be negating any value/class that presents as == to T{}. It would be safer to only negate those strictly less than T{} \$\endgroup\$ – rolfl Aug 15 '14 at 23:56
  • 1
    \$\begingroup\$ Hm...good point. Traditionally, you only require < to be defined, so you'd want to switch the order of operands rather than the operator though. return a < T{} ? -a : a; \$\endgroup\$ – Jerry Coffin Aug 16 '14 at 4:32
3
\$\begingroup\$

The only point I'd make it to look into using boost::call_traits ( see here ). Instead of always passing by const T&, this will select the "best" way to pass a parameter: by const T for small, built in types (such as int), and by const T& for class types.

template<typename T>
constexpr auto abs(boost::call_traits<T>::param_type value)
    -> T
{ ... }
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.