I found an alternative, it doesn't fit into comments, so I'm posting an answer.
I think it can also be done by using std::nextafter
and std::nexttoward
:
template<typename FT>
std::enable_if_t<std::is_floating_point_v<FT>, FT>
ulp(FT x)
{
if (x > 0)
return std::nexttoward(x, std::numeric_limits<FT>::infinity()) - x;
else
return x - std::nexttoward(x, -std::numeric_limits<FT>::infinity());
}
This is valid if nexttoward
results in a floating point number that is next toward INF
if the number is positive, and toward -INF if the number is negative. Because this number is a floating point number, the distance from x is going to be ulp(x)
and it doesn't have a rounding error.
Here is a test program for this idea, it seems to work without using power functions and computing exponents manually (it's probably doing something like that under the hood). I tested it by checking:
// Check ulp by adding 0.5ulp to x and let the rounding to nearest round to x.
assert((x + (0.5 *ulpx) == x));
Here is the MWE for it:
#include <cmath>
#include <cassert>
#include <iostream>
#include <iomanip>
using namespace std;
template<typename FT>
std::enable_if_t<std::is_floating_point_v<FT>, FT>
ulp(FT x)
{
if (x > 0)
return std::nexttoward(x, numeric_limits<FT>::infinity()) - x;
else
return x - std::nexttoward(x, -numeric_limits<FT>::infinity());
}
void test_ulp(double x)
{
double ulpx = ulp(x);
std::cout << "ulpx = " << std::setprecision(25) << ulpx << std::endl;
double xNext = x + ulpx;
// Check ulp by adding 0.5ulp to x and let the rounding to nearest round to x.
if (x > 0)
assert((x + (0.5 *ulpx) == x));
else
assert((x - (0.5 *ulpx) == x));
std::cout << std::setprecision(25) << x << " + " << ulpx
<< " == " << xNext << std::endl;
}
int main()
{
// Denormalized test.
test_ulp(0.);
// Epsilon test.
test_ulp(1.);
// Random number test. :)
test_ulp(42.);
// Epsilon test.
test_ulp(-1.);
}
Edit: tried to find what nexttoward
does, looked into gcc STL source code and only found this:
#ifndef __CORRECT_ISO_CPP11_MATH_H_PROTO_FP
constexpr float
nexttoward(float __x, long double __y)
{ return __builtin_nexttowardf(__x, __y); }
constexpr long double
nexttoward(long double __x, long double __y)
{ return __builtin_nexttowardl(__x, __y); }
#endif
#ifndef __CORRECT_ISO_CPP11_MATH_H_PROTO_INT
template<typename _Tp>
constexpr typename __gnu_cxx::__enable_if<__is_integer<_Tp>::__value,
double>::__type
nexttoward(_Tp __x, long double __y)
{ return __builtin_nexttoward(__x, __y); }
#endif