Skip to content

Commit 242020e

Browse files
authored
Merge pull request intel#171 from elbeno/add-rollover
✨ Add `rollover_t`
2 parents 4329991 + 9ebf17f commit 242020e

File tree

9 files changed

+487
-0
lines changed

9 files changed

+487
-0
lines changed

docs/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ include::optional.adoc[]
3737
include::panic.adoc[]
3838
include::priority.adoc[]
3939
include::ranges.adoc[]
40+
include::rollover.adoc[]
4041
include::span.adoc[]
4142
include::static_assert.adoc[]
4243
include::tuple.adoc[]

docs/intro.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ The following headers are available:
6363
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/panic.hpp[`panic.hpp`]
6464
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/priority.hpp[`priority.hpp`]
6565
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/ranges.hpp[`ranges.hpp`]
66+
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/rollover.hpp[`rollover.hpp`]
6667
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/span.hpp[`span.hpp`]
6768
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/static_assert.hpp[`static_assert.hpp`]
6869
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/tuple.hpp[`tuple.hpp`]

docs/rollover.adoc

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
2+
== `rollover.hpp`
3+
4+
https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/rollover.hpp[`rollover.hpp`]
5+
provides a class template `rollover_t` that is intended to act like an unsigned
6+
integral type, but with semantics that include the ability to roll-over on overflow.
7+
8+
A `rollover_t` can be instantiated with any unsigned integral type:
9+
10+
[source,cpp]
11+
----
12+
// explicit type
13+
auto x = stdx::rollover_t<std::uint8_t>{};
14+
15+
// deduced type: must be unsigned
16+
auto y = stdx::rollover_t{1u}; // rollover_t<unsigned int>
17+
----
18+
19+
It supports all the usual arithmetic operations (`+` `-` `*` `/` `%`) and
20+
behaves much like an unsigned integral type, with defined overflow and underflow
21+
semantics.
22+
23+
=== Comparison semantics
24+
25+
`rollover_t` supports equality, but the comparison operators (`<` `<​=` `>` `>=`)
26+
are deleted. Instead, `cmp_less` is provided, with different semantics. A
27+
`rollover_t` considers itself to be in the middle of a rolling window where half
28+
the bit-range is always lower and half is higher.
29+
30+
For instance, imagine a 3-bit unsigned integral type. There are eight values of
31+
this type: `0` `1` `2` `3` `4` `5` `6` `7`. Let's call the `rollover_t` over
32+
this type `R`.
33+
34+
CAUTION: `operator<` on `rollover_t` is not antisymmetric!
35+
36+
For any value, there are always four values (half the bit-space) less than it,
37+
and four values greater than it. And of course it is equal to itself. e.g. for
38+
the `R` value `5`:
39+
40+
- `1`, `2`, `3`, `4` are all less than `5`
41+
- `6`, `7`, `0`, `1` are all greater than `5`
42+
43+
i.e. `cmp_less(R{1u}, R{5u})` is `true`. And `cmp_less(R{5u}, R{1u})` is true.
44+
45+
Effectively any value partitions the cyclic space in this way.
46+
47+
CAUTION: `operator<` on `rollover_t` is not transitive!
48+
49+
Also, the following are all true for `R`:
50+
51+
- `1` < `3`
52+
- `3` < `5`
53+
- `5` < `7`
54+
- `7` < `1`
55+
56+
The cyclic nature of the space means that `operator<` is neither antisymmetric
57+
nor transitive! (Lack of antisymmetry might be viewed as a special case of
58+
non-transitivity.)
59+
60+
This means we need to take care with operations that assume the antisymmetric
61+
and transitive nature of the less-than operation. In particular `cmp_less` does
62+
_not_ define a
63+
https://en.cppreference.com/w/cpp/concepts/strict_weak_order[strict weak order]
64+
-- which is why `operator<` and friends are deleted. In the absence of data
65+
constraints, `rollover_t` cannot be sorted with `std::sort`.
66+
67+
NOTE: A suitable constraint might be that the data lies completely within half
68+
the bit-range: in that case, `cmp_less` _would_ have the correct properties and
69+
_could_ be used as a
70+
https://en.cppreference.com/w/cpp/named_req/LessThanComparable[comparator]
71+
argument to `std::sort`. As always in C++, we protect against Murphy, not
72+
Machiavelli.
73+
74+
=== Use with `std::chrono`
75+
76+
`rollover_t` is intended for use in applications like timers which may be
77+
modelled as a 32-bit counter that rolls over. In that case, it makes sense to
78+
consider a sliding window centred around "now" where half the bit-space is in
79+
the past, and half is in the future. Under such a scheme, in general it is
80+
undefined to schedule an event more than 31 bits-worth in the future.
81+
82+
[source,cpp]
83+
----
84+
// 32-bit rollover type
85+
using ro_t = stdx::rollover_t<std::uint32_t>;
86+
// Used with a microsecond resolution
87+
using ro_duration_t = std::chrono::duration<ro_t, std::micro>;
88+
using ro_time_point_t = std::chrono::time_point<std::chrono::local_t, ro_duration_t>;
89+
----
90+
91+
This allows us to benefit from the typed time handling of `std::chrono`, and use
92+
`cmp_less` for specialist applications like keeping a sorted list of timer
93+
tasks, where we have the constraint that we never schedule an event beyond a
94+
certain point in the future.

include/stdx/rollover.hpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#pragma once
2+
3+
#include <stdx/concepts.hpp>
4+
5+
#include <type_traits>
6+
7+
namespace stdx {
8+
inline namespace v1 {
9+
template <typename T> struct rollover_t {
10+
static_assert(unsigned_integral<T>,
11+
"Argument to rollover_t must be an unsigned integral type.");
12+
using underlying_t = T;
13+
14+
constexpr rollover_t() = default;
15+
template <typename U,
16+
typename = std::enable_if_t<std::is_convertible_v<U, T>>>
17+
constexpr explicit rollover_t(U u) : value{static_cast<underlying_t>(u)} {}
18+
template <typename U,
19+
typename = std::enable_if_t<std::is_convertible_v<U, T>>>
20+
constexpr explicit rollover_t(rollover_t<U> u)
21+
: rollover_t{static_cast<U>(u)} {}
22+
23+
[[nodiscard]] constexpr auto as_underlying() const -> underlying_t {
24+
return value;
25+
}
26+
constexpr explicit operator underlying_t() const { return value; }
27+
28+
[[nodiscard]] constexpr auto operator+() const -> rollover_t {
29+
return *this;
30+
}
31+
[[nodiscard]] constexpr auto operator-() const -> rollover_t {
32+
return rollover_t{static_cast<underlying_t>(-value)};
33+
}
34+
35+
constexpr auto operator++() -> rollover_t & {
36+
++value;
37+
return *this;
38+
}
39+
constexpr auto operator++(int) -> rollover_t { return rollover_t{value++}; }
40+
41+
constexpr auto operator--() -> rollover_t & {
42+
--value;
43+
return *this;
44+
}
45+
constexpr auto operator--(int) -> rollover_t { return rollover_t{value--}; }
46+
47+
constexpr auto operator+=(rollover_t other) -> rollover_t & {
48+
value += other.value;
49+
return *this;
50+
}
51+
constexpr auto operator-=(rollover_t other) -> rollover_t & {
52+
value -= other.value;
53+
return *this;
54+
}
55+
constexpr auto operator*=(rollover_t other) -> rollover_t & {
56+
value *= other.value;
57+
return *this;
58+
}
59+
constexpr auto operator/=(rollover_t other) -> rollover_t & {
60+
value /= other.value;
61+
return *this;
62+
}
63+
constexpr auto operator%=(rollover_t other) -> rollover_t & {
64+
value %= other.value;
65+
return *this;
66+
}
67+
68+
private:
69+
[[nodiscard]] constexpr friend auto operator==(rollover_t lhs,
70+
rollover_t rhs) -> bool {
71+
return lhs.value == rhs.value;
72+
}
73+
[[nodiscard]] constexpr friend auto operator!=(rollover_t lhs,
74+
rollover_t rhs) -> bool {
75+
return not(lhs == rhs);
76+
}
77+
78+
constexpr friend auto operator<(rollover_t, rollover_t) -> bool = delete;
79+
constexpr friend auto operator<=(rollover_t, rollover_t) -> bool = delete;
80+
constexpr friend auto operator>(rollover_t, rollover_t) -> bool = delete;
81+
constexpr friend auto operator>=(rollover_t, rollover_t) -> bool = delete;
82+
83+
[[nodiscard]] constexpr friend auto cmp_less(rollover_t lhs,
84+
rollover_t rhs) -> bool {
85+
constexpr auto mid = static_cast<underlying_t>(~underlying_t{}) / 2;
86+
return static_cast<underlying_t>(lhs.value - rhs.value) > mid;
87+
}
88+
89+
[[nodiscard]] constexpr friend auto
90+
operator+(rollover_t lhs, rollover_t rhs) -> rollover_t {
91+
lhs += rhs;
92+
return lhs;
93+
}
94+
[[nodiscard]] constexpr friend auto
95+
operator-(rollover_t lhs, rollover_t rhs) -> rollover_t {
96+
lhs -= rhs;
97+
return lhs;
98+
}
99+
[[nodiscard]] constexpr friend auto
100+
operator*(rollover_t lhs, rollover_t rhs) -> rollover_t {
101+
lhs *= rhs;
102+
return lhs;
103+
}
104+
[[nodiscard]] constexpr friend auto
105+
operator/(rollover_t lhs, rollover_t rhs) -> rollover_t {
106+
lhs /= rhs;
107+
return lhs;
108+
}
109+
[[nodiscard]] constexpr friend auto
110+
operator%(rollover_t lhs, rollover_t rhs) -> rollover_t {
111+
lhs %= rhs;
112+
return lhs;
113+
}
114+
115+
underlying_t value{};
116+
};
117+
118+
template <typename T> rollover_t(T) -> rollover_t<T>;
119+
} // namespace v1
120+
} // namespace stdx
121+
122+
template <typename T, typename U>
123+
struct std::common_type<stdx::rollover_t<T>, stdx::rollover_t<U>> {
124+
using type = stdx::rollover_t<std::common_type_t<T, U>>;
125+
};
126+
127+
template <typename T, typename I>
128+
struct std::common_type<stdx::rollover_t<T>, I> {
129+
using type =
130+
stdx::rollover_t<std::common_type_t<T, std::make_unsigned_t<I>>>;
131+
};

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ add_tests(
5555
priority
5656
ranges
5757
remove_cvref
58+
rollover
5859
span
5960
to_underlying
6061
type_map

test/fail/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ add_fail_tests(
1313
for_each_n_args_bad_size
1414
optional_without_tombstone
1515
optional_integral_with_tombstone_traits
16+
rollover_less_than
17+
rollover_signed
1618
span_insufficient_storage
1719
span_larger_prefix
1820
span_larger_suffix

test/fail/rollover_less_than.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include <stdx/rollover.hpp>
2+
3+
// EXPECT: deleted (operator|function)
4+
5+
auto main() -> int {
6+
using X = stdx::rollover_t<unsigned int>;
7+
[[maybe_unused]] auto cmp = X{} < X{1u};
8+
}

test/fail/rollover_signed.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#include <stdx/rollover.hpp>
2+
3+
// EXPECT: Argument to rollover_t must be an unsigned integral type
4+
5+
auto main() -> int {
6+
using X = stdx::rollover_t<int>;
7+
[[maybe_unused]] X x{};
8+
}

0 commit comments

Comments
 (0)