Skip to content

Commit 579d57d

Browse files
committed
Fix an error where midpoint(k,k) for small but not near denormal dif not equal k by implementing a simpler algorithm. Add comments.
1 parent c294759 commit 579d57d

8 files changed

+405
-17
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
12
test_lerp
23
test_midpoint
4+
test_midpoint_new

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ These just got voted into C++20!
1111
float t = 0.75;
1212
auto x = std::lerp(a, b, t);
1313

14+
Jonathan Wakely just put this in (3/3/2019)! W00t!
15+
16+
But I can still play around. ;-)
17+
Plus, IMHO, we should have these for complex as well.

lerp.h

+6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
namespace std
77
{
88

9+
/**
10+
* Linearly interpolate from @c a to @c b by fraction @c t.
11+
* @f[
12+
* lerp(t; a, b) = t b + (1 - t) a
13+
* @f]
14+
*/
915
template<typename _Float>
1016
constexpr std::enable_if_t<std::is_floating_point_v<_Float>, _Float>
1117
lerp(_Float __a, _Float __b, _Float __t)

midpoint.h

+36-15
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,63 @@
77
namespace std
88
{
99

10+
/**
11+
* Computing the (integer) midpoint of two integer values via
12+
* (a+b)/2
13+
* can cause overflow for signed or unsigned integers as well
14+
* as for floating-point values.
15+
*/
1016
template<typename _Int>
1117
constexpr std::enable_if_t<std::is_integral_v<_Int>
1218
&& !std::is_same_v<_Int, bool>, _Int>
1319
midpoint(_Int __a, _Int __b) noexcept
1420
{
1521
using _UInt = std::make_unsigned_t<_Int>;
16-
/*
17-
const auto __ua = _UInt(__a);
18-
const auto __ub = _UInt(__b);
19-
const auto __del = (__ub > __ua ? +(__ub - __ua) : -(__ua - __ub));
20-
return _Int(__ua + __del / 2);
21-
*/
22-
return _Int(_UInt(__a) + (_UInt(__b) - _UInt(__a)) / 2);
22+
return __a > __b
23+
? __a - _Int(_UInt(__a) - _UInt(__b) / 2)
24+
: __a + _Int(_UInt(__b) - _UInt(__a) / 2);
2325
}
2426

25-
// What about infinity?
27+
/**
28+
* Computing the (integer) midpoint of two float values via
29+
* (a+b)/2
30+
* can cause overflow.
31+
* The choice
32+
* a + (b - a)/2
33+
* is not correctly rounded (by rounding in the subtraction and the addition).
34+
* A third choice
35+
* a/2 + b/2
36+
* prevents overflow but is not correctly rounded for subnormal inputs
37+
* (due to rounding each separately).
38+
*
39+
* What about infinity?
40+
*/
2641
template<typename _Float>
2742
constexpr std::enable_if_t<std::is_floating_point_v<_Float>, _Float>
2843
midpoint(_Float __a, _Float __b) noexcept
2944
{
45+
constexpr auto __max = std::numeric_limits<_Float>::max() / 2;
3046
if (std::isnan(__a) || std::isnan(__b))
3147
return std::numeric_limits<_Float>::quiet_NaN();
48+
else if (std::abs(__a) < __max && std::abs(__b) < __max)
49+
return (__a + __b) / 2;
3250
else
33-
return std::isnormal(__a) && std::isnormal(__b)
34-
? __a / 2 + __b / 2
35-
: (__a + __b) / 2;
51+
return __a / 2 + __b / 2;
3652
}
3753

54+
/**
55+
* Midpoint between two pointers.
56+
*
57+
* As with integers, when the midpoint lies between two pointer values
58+
* the one closer to a is chosen; for the usual case of a<b,
59+
* this is compatible with the usual half-open ranges by selecting
60+
* a when [a,b) is [a,a+1).
61+
*/
3862
template<typename _Tp>
3963
_Tp*
4064
midpoint(_Tp* __a, _Tp* __b)
4165
{
42-
//using _Iptr = std::intptr_t;
43-
//return static_cast<_Tp*>(__a + std::midpoint(_Iptr(__a), _Iptr(__b)));
44-
const auto __diff = (__b - __a) / 2;
45-
return __a + __diff;
66+
return __a + (__b - __a) / 2;
4667
}
4768

4869
} // namespace std

papers/P0811R3: Well-behaved interpolation for numbers and pointers.html

+201
Large diffs are not rendered by default.

test_lerp.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
/home/ed/bin/bin/g++ -std=c++17 -g -Wall -Wextra -o test_lerp test_lerp.cpp
2+
/home/ed/bin/bin/g++ -std=c++20 -g -Wall -Wextra -o test_lerp test_lerp.cpp
33
*/
44

55
#include <limits>

test_midpoint.cpp

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
2-
/home/ed/bin/bin/g++ -std=c++17 -g -Wall -Wextra -o test_midpoint test_midpoint.cpp
2+
/home/ed/bin/bin/g++ -std=c++20 -g -Wall -Wextra -o test_midpoint test_midpoint.cpp
3+
LD_LIBRARY_PATH=/home/ed/bin/lib64 ./test_midpoint
34
*/
45

56
#include <limits>
@@ -51,6 +52,22 @@ template<typename Tp>
5152
return true;
5253
}
5354

55+
bool
56+
test_tiny()
57+
{
58+
auto d = std::numeric_limits<double>::max_digits10;
59+
std::cout.precision(d);
60+
const double K = 4.4501477170144023e-308;
61+
const double X[]
62+
{
63+
K, std::midpoint(K, K)
64+
};
65+
std::cout << "X[0] = " << X[0] << '\n';
66+
std::cout << "X[1] = " << X[1] << '\n';
67+
68+
return 0 == (X[1] - X[0]);
69+
}
70+
5471
int
5572
main()
5673
{
@@ -75,5 +92,7 @@ main()
7592
std::cout << "\ntest_ptr\n";
7693
ok = ok && test_ptr<int>();
7794

95+
ok = test_tiny();
96+
7897
return ok;
7998
}

test_midpoint_new.cpp

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
/home/ed/bin/bin/g++ -std=c++20 -o test_midpoint_new test_midpoint_new.cpp
3+
LD_LIBRARY_PATH=/home/ed/bin/lib64 ./test_midpoint_new
4+
*/
5+
6+
#include <cmath>
7+
#include <numeric>
8+
#include <iostream>
9+
#include <limits>
10+
#include <cstdint>
11+
#include <type_traits>
12+
13+
/// @see https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0811r3.html
14+
15+
namespace phlegm
16+
{
17+
18+
/**
19+
* Computing the (integer) midpoint of two integer values via
20+
* (a+b)/2
21+
* can cause overflow for signed or unsigned integers as well
22+
* as for floating-point values.
23+
*/
24+
template<typename Int>
25+
constexpr std::enable_if_t<std::is_integral_v<Int>
26+
&& !std::is_same_v<Int, bool>, Int>
27+
midpoint(Int a, Int b) noexcept
28+
{
29+
using UInt = std::make_unsigned_t<Int>;
30+
return a > b
31+
? a - Int(UInt(a) - UInt(b) / 2)
32+
: a + Int(UInt(b) - UInt(a) / 2);
33+
}
34+
35+
/**
36+
* Computing the (integer) midpoint of two float values via
37+
* (a+b)/2
38+
* can cause overflow.
39+
* The choice
40+
* a + (b - a)/2
41+
* is not correctly rounded (by rounding in the subtraction and the addition).
42+
* A third choice
43+
* a/2 + b/2
44+
* prevents overflow but is not correctly rounded for subnormal inputs
45+
* (due to rounding each separately).
46+
*
47+
* What about infinity?
48+
*/
49+
template<typename Float>
50+
constexpr std::enable_if_t<std::is_floating_point_v<Float>, Float>
51+
midpoint(Float a, Float b) noexcept
52+
{
53+
constexpr auto max = std::numeric_limits<Float>::max() / 2;
54+
if (std::isnan(a) || std::isnan(b))
55+
return std::numeric_limits<Float>::quiet_NaN();
56+
else if (std::abs(a) < max && std::abs(b) < max)
57+
return (a + b) / 2;
58+
else
59+
return a / 2 + b / 2;
60+
}
61+
62+
/**
63+
* Midpoint between two pointers.
64+
*
65+
* As with integers, when the midpoint lies between two pointer values
66+
* the one closer to a is chosen; for the usual case of a<b,
67+
* this is compatible with the usual half-open ranges by selecting
68+
* a when [a,b) is [a,a+1).
69+
*/
70+
template<typename Tp>
71+
constexpr std::enable_if_t<std::is_object_v<Tp>, Tp*>
72+
midpoint(Tp* a, Tp* b)
73+
{
74+
return a + (b - a) / 2;
75+
}
76+
77+
} // namespace phlegm
78+
79+
int
80+
main()
81+
{
82+
auto d = std::numeric_limits<double>::max_digits10;
83+
std::cout.precision(d);
84+
const double K = 4.4501477170144023e-308;
85+
double X[] = {
86+
K, phlegm::midpoint(K, K)
87+
};
88+
bool nK = std::isnormal(K);
89+
bool mK = std::isnormal(phlegm::midpoint(K, K));
90+
std::cout << ' ' << X[0] << ' ' << X[1] << '\n';
91+
auto pK = phlegm::midpoint(K, K);
92+
auto dnorm = std::numeric_limits<double>::denorm_min();
93+
std::cout << ' ' << dnorm << '\n';
94+
auto deps = std::numeric_limits<double>::epsilon();
95+
std::cout << ' ' << deps << '\n';
96+
auto diff = X[0] - X[1];
97+
std::cout << ' ' << diff << '\n';
98+
auto min = std::numeric_limits<double>::min();
99+
std::cout << ' ' << min << '\n';
100+
std::cout << ' ' << min/2 << '\n';
101+
}
102+
103+
/*
104+
phlegm::midpoint<double> (a=4.4501477170144023e-308, b=4.4501477170144023e-308) at test_midpoint.cpp:35
105+
35 if (std::isnan(a) || std::isnan(b))
106+
(gdb) n
107+
38 return std::isnormal(a) && std::isnormal(b)
108+
(gdb)
109+
39 ? a / 2 + b / 2
110+
(gdb)
111+
38 return std::isnormal(a) && std::isnormal(b)
112+
(gdb) p std::isnormal(a)
113+
$1 = true
114+
(gdb) p std::isnormal(b)
115+
$2 = true
116+
(gdb) p a / 2
117+
$3 = 2.2250738585072014e-308
118+
(gdb) p b / 2
119+
$4 = 2.2250738585072014e-308
120+
(gdb) p a / 2 + b / 2
121+
$5 = 4.4501477170144028e-308
122+
(gdb) p (a+b)/2
123+
$6 = 4.4501477170144023e-308
124+
(gdb) p a
125+
$7 = 4.4501477170144023e-308
126+
(gdb)
127+
128+
(gdb) p pK
129+
$8 = 4.4501477170144028e-308
130+
131+
(gdb) p K/2+K/2
132+
$13 = 4.4501477170144028e-308
133+
(gdb) p (K+K)/2
134+
$14 = 4.4501477170144023e-308
135+
*/

0 commit comments

Comments
 (0)