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. Usingstd::abs
when possible will probably generate an optimized executable. That said,std::abs
lacksconstexpr
, 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-levelabs
may also take their parameter byconst&
.Some types only provide
operator<
to represent the ordering, but not the other relational operators. Therefore, callingoperator<
in the generic algorithm is more likely to work.I chose to use
T{}
instead of0
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)?
2 Answers 2
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.
-
\$\begingroup\$ That's actually a question: if it is not observable, does it matter? \$\endgroup\$Morwenn– Morwenn2014年08月15日 16:04:01 +00:00Commented Aug 15, 2014 at 16:04
-
2\$\begingroup\$ @Morwenn It's observable. To test, divide by zero!
1.0 / 0.0
isinf
.1.0 / -0.0
is-inf
. \$\endgroup\$200_success– 200_success2014年08月15日 16:42:08 +00:00Commented Aug 15, 2014 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– Morwenn2014年08月15日 23:53:07 +00:00Commented Aug 15, 2014 at 23:53 -
\$\begingroup\$ @Morwenn - you will still be negating any value/class that presents as
==
toT{}
. It would be safer to only negate those strictly less thanT{}
\$\endgroup\$rolfl– rolfl2014年08月15日 23:56:18 +00:00Commented Aug 15, 2014 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– Jerry Coffin2014年08月16日 04:32:03 +00:00Commented Aug 16, 2014 at 4:32
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
{ ... }