7

I'm currently writing a templated helper method that can convert C numbers in general (including unsigned long long) to mpz_class numbers in the GMP library. In between, there is a call to std::abs.

However, it turns out that for on C++17 (g++ 6.3.1),

#include <iostream>
#include <cmath>

int main()
{
    std::cout << (unsigned long long)std::abs(9484282305798401ull);
}

gives an incorrect output of 9484282305798400.

As I understood from cmath, std::abs first casts the argument into a double.

According to the C++ docs, double has 52 mantissa bits, which means that the maximum integer value that I must be strictly less than 2^52 = 4503599627370496 before any loss of precision.

Am I correct to say that since 9484282305798401 exceeds this limit, std::abs ends up discarding precision to give an incorrect answer?

To clarify, I'm absolutely aware that it makes completely no sense to ask for the absolute value of an unsigned integer; However, I would like the templated function work for general C numbers, instead of having to specifically create a specialization for each signed and unsigned type separately.

Yiyuan Lee
  • 659
  • 8
  • 23
  • 3
    If I may, whats the point of calling `abs` on an unsigned type? – stelioslogothetis May 31 '17 at 14:30
  • I'm writing a templated function that goes through some arithmetic before calling the mpz_import class of the GMP library; In between there is a call to std::abs that may be required for signed numbers. – Yiyuan Lee May 31 '17 at 14:43
  • Related to [Is std::abs(0u) ill-formed?](https://stackoverflow.com/q/29750946/1708801) – Shafik Yaghmour May 31 '17 at 16:24
  • 1
    Note that whilst IEEE 754 double precision does use 52 bits to store the mantissa, there are effectively 53 bits since the value is normalized and there is an 'implicit 1' (see this SO post : https://stackoverflow.com/questions/4930269/floating-point-the-leading-1-is-implicit-in-the-significand-huh) – Paul Floyd Jun 01 '17 at 12:55

3 Answers3

9

Your program is ill-formed. From [c.math.abs]/29.9.2.3:

If abs() is called with an argument of type X for which is_­unsigned_­v<X> is true and if X cannot be converted to int by integral promotion, the program is ill-formed.

Compilers should have to warn you about this though.

It also doesn't really make sense to call std::abs on an unsigned type anyways.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • Ill-formed requires a diagnostic, see [details in my answer here](https://stackoverflow.com/a/31685448/1708801) it does not have to be an error though. UB and ill-formed NDR on the other hand do not require a diagnostic – Shafik Yaghmour May 31 '17 at 16:30
  • This is one of the neat things about UB and constexpr, [UB is ill-formed in a constexpr and therefore requires a diagnostic](https://stackoverflow.com/q/21319413/1708801). – Shafik Yaghmour May 31 '17 at 16:35
  • @Shafik Yes, you're totally right. I misread the explanation of ill-formed :) – Rakete1111 May 31 '17 at 18:22
  • It has total sense to cast `std::abs` on an unsigned type when you're dealing with templates. In that case, it should just be an identity function. – Piotr Siupa Jan 01 '23 at 18:18
5

First off, what you are doing doesn't really make sense out of context (getting the absolute value of an unsigned type). But I digress.

The code you have posted doesn't compile. At least not in the compiler I used (whichever one repl.it uses). Instead it complains of an ambiguous overload. Even if it did compile, it would cast the unsigned long long to a different type which cannot support its actual value (in this case double).

Changing abs to llabs like so:

std::cout << (unsigned long long)std::llabs(9484282305798401ull);

..both makes it compile and produces an accurate result. See the documentation of the different abs functions for integer types here.

stelioslogothetis
  • 9,371
  • 3
  • 28
  • 53
  • Older versions of [libstdc++ used to accept this code](https://stackoverflow.com/questions/44287410/why-is-stdabs9484282305798401ull-9484282305798400#comment75586599_44287410) I suspect it will be a while before those older versions mostly go away. – Shafik Yaghmour May 31 '17 at 18:02
  • True, however OP stated that they are using c++17, and the error is present from c++11. But for completeness' sake I will include this in my answer. – stelioslogothetis May 31 '17 at 18:04
  • [gcc 6.3 using c++1z and -pedantic](https://wandbox.org/permlink/dLCav79n8E2li63Q) no diagnostic. – Shafik Yaghmour May 31 '17 at 18:05
  • Slight problem w/ using `llabs` is that `The behavior is undefined if the result cannot be represented by the return type` – Shafik Yaghmour May 31 '17 at 19:45
3

You can create your own overload of abs if you want to manage unsigned types in a way that is different for what the standard library function does:

#include <cmath>
#include <type_traits>

namespace my {

template <class S>
auto abs (S x) -> typename std::enable_if<std::is_signed<S>::value,
                                          decltype(std::abs(x))>::type
{
    return std::abs(x);
}

template <class U>
auto abs (U x) -> typename std::enable_if<std::is_unsigned<U>::value, U>::type
{
    return x;
}

} // namespace my

Then

std::cout << my::abs(9484282305798401ull) << '\n'              // -> 9484282305798401
          << my::abs(-3.14159) << '\n'                         // -> 3.14159
          << my::abs(std::numeric_limits<char>::min()) << '\n' // -> 128
          << my::abs(std::numeric_limits<int>::min()) << '\n'  // -> -2147483648

Please, note that std::abs promotes chars (signed in my implementation), but due to 2's complement representation of ints, fails to retrieve the absolute value of INT_MIN.

Bob__
  • 12,361
  • 3
  • 28
  • 42
  • This would be even better if you use SFINAE, so that types that can't​ promote to unsigned long long also work – Rakete1111 May 31 '17 at 18:23