Skip to content

Commit

Permalink
✨ Add rollover_t
Browse files Browse the repository at this point in the history
Problem:
- When dealing with a timer register that rolls over, it is useful to have a
  type that knows how to handle that.

Solution:
- Add `rollover_t`.
- `rollover_t` behaves like an unsigned integral type, with interesting
  comparison semantics.
  • Loading branch information
elbeno committed Nov 11, 2024
1 parent 4329991 commit 9ebf17f
Show file tree
Hide file tree
Showing 9 changed files with 487 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ include::optional.adoc[]
include::panic.adoc[]
include::priority.adoc[]
include::ranges.adoc[]
include::rollover.adoc[]
include::span.adoc[]
include::static_assert.adoc[]
include::tuple.adoc[]
Expand Down
1 change: 1 addition & 0 deletions docs/intro.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ The following headers are available:
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/panic.hpp[`panic.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/priority.hpp[`priority.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/ranges.hpp[`ranges.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/rollover.hpp[`rollover.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/span.hpp[`span.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/static_assert.hpp[`static_assert.hpp`]
* https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/tuple.hpp[`tuple.hpp`]
Expand Down
94 changes: 94 additions & 0 deletions docs/rollover.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@

== `rollover.hpp`

https://github.com/intel/cpp-std-extensions/blob/main/include/stdx/rollover.hpp[`rollover.hpp`]
provides a class template `rollover_t` that is intended to act like an unsigned
integral type, but with semantics that include the ability to roll-over on overflow.

A `rollover_t` can be instantiated with any unsigned integral type:

[source,cpp]
----
// explicit type
auto x = stdx::rollover_t<std::uint8_t>{};
// deduced type: must be unsigned
auto y = stdx::rollover_t{1u}; // rollover_t<unsigned int>
----

It supports all the usual arithmetic operations (`+` `-` `*` `/` `%`) and
behaves much like an unsigned integral type, with defined overflow and underflow
semantics.

=== Comparison semantics

`rollover_t` supports equality, but the comparison operators (`<` `<​=` `>` `>=`)
are deleted. Instead, `cmp_less` is provided, with different semantics. A
`rollover_t` considers itself to be in the middle of a rolling window where half
the bit-range is always lower and half is higher.

For instance, imagine a 3-bit unsigned integral type. There are eight values of
this type: `0` `1` `2` `3` `4` `5` `6` `7`. Let's call the `rollover_t` over
this type `R`.

CAUTION: `operator<` on `rollover_t` is not antisymmetric!

For any value, there are always four values (half the bit-space) less than it,
and four values greater than it. And of course it is equal to itself. e.g. for
the `R` value `5`:

- `1`, `2`, `3`, `4` are all less than `5`
- `6`, `7`, `0`, `1` are all greater than `5`

i.e. `cmp_less(R{1u}, R{5u})` is `true`. And `cmp_less(R{5u}, R{1u})` is true.

Effectively any value partitions the cyclic space in this way.

CAUTION: `operator<` on `rollover_t` is not transitive!

Also, the following are all true for `R`:

- `1` < `3`
- `3` < `5`
- `5` < `7`
- `7` < `1`

The cyclic nature of the space means that `operator<` is neither antisymmetric
nor transitive! (Lack of antisymmetry might be viewed as a special case of
non-transitivity.)

This means we need to take care with operations that assume the antisymmetric
and transitive nature of the less-than operation. In particular `cmp_less` does
_not_ define a
https://en.cppreference.com/w/cpp/concepts/strict_weak_order[strict weak order]
-- which is why `operator<` and friends are deleted. In the absence of data
constraints, `rollover_t` cannot be sorted with `std::sort`.

NOTE: A suitable constraint might be that the data lies completely within half
the bit-range: in that case, `cmp_less` _would_ have the correct properties and
_could_ be used as a
https://en.cppreference.com/w/cpp/named_req/LessThanComparable[comparator]
argument to `std::sort`. As always in C++, we protect against Murphy, not
Machiavelli.

=== Use with `std::chrono`

`rollover_t` is intended for use in applications like timers which may be
modelled as a 32-bit counter that rolls over. In that case, it makes sense to
consider a sliding window centred around "now" where half the bit-space is in
the past, and half is in the future. Under such a scheme, in general it is
undefined to schedule an event more than 31 bits-worth in the future.

[source,cpp]
----
// 32-bit rollover type
using ro_t = stdx::rollover_t<std::uint32_t>;
// Used with a microsecond resolution
using ro_duration_t = std::chrono::duration<ro_t, std::micro>;
using ro_time_point_t = std::chrono::time_point<std::chrono::local_t, ro_duration_t>;
----

This allows us to benefit from the typed time handling of `std::chrono`, and use
`cmp_less` for specialist applications like keeping a sorted list of timer
tasks, where we have the constraint that we never schedule an event beyond a
certain point in the future.
131 changes: 131 additions & 0 deletions include/stdx/rollover.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#pragma once

#include <stdx/concepts.hpp>

#include <type_traits>

namespace stdx {
inline namespace v1 {
template <typename T> struct rollover_t {
static_assert(unsigned_integral<T>,
"Argument to rollover_t must be an unsigned integral type.");
using underlying_t = T;

constexpr rollover_t() = default;
template <typename U,
typename = std::enable_if_t<std::is_convertible_v<U, T>>>
constexpr explicit rollover_t(U u) : value{static_cast<underlying_t>(u)} {}
template <typename U,
typename = std::enable_if_t<std::is_convertible_v<U, T>>>
constexpr explicit rollover_t(rollover_t<U> u)
: rollover_t{static_cast<U>(u)} {}

[[nodiscard]] constexpr auto as_underlying() const -> underlying_t {
return value;
}
constexpr explicit operator underlying_t() const { return value; }

[[nodiscard]] constexpr auto operator+() const -> rollover_t {
return *this;
}
[[nodiscard]] constexpr auto operator-() const -> rollover_t {
return rollover_t{static_cast<underlying_t>(-value)};
}

constexpr auto operator++() -> rollover_t & {
++value;
return *this;
}
constexpr auto operator++(int) -> rollover_t { return rollover_t{value++}; }

constexpr auto operator--() -> rollover_t & {
--value;
return *this;
}
constexpr auto operator--(int) -> rollover_t { return rollover_t{value--}; }

constexpr auto operator+=(rollover_t other) -> rollover_t & {
value += other.value;
return *this;
}
constexpr auto operator-=(rollover_t other) -> rollover_t & {
value -= other.value;
return *this;
}
constexpr auto operator*=(rollover_t other) -> rollover_t & {
value *= other.value;
return *this;
}
constexpr auto operator/=(rollover_t other) -> rollover_t & {
value /= other.value;
return *this;
}
constexpr auto operator%=(rollover_t other) -> rollover_t & {
value %= other.value;
return *this;
}

private:
[[nodiscard]] constexpr friend auto operator==(rollover_t lhs,
rollover_t rhs) -> bool {
return lhs.value == rhs.value;
}
[[nodiscard]] constexpr friend auto operator!=(rollover_t lhs,
rollover_t rhs) -> bool {
return not(lhs == rhs);
}

constexpr friend auto operator<(rollover_t, rollover_t) -> bool = delete;
constexpr friend auto operator<=(rollover_t, rollover_t) -> bool = delete;
constexpr friend auto operator>(rollover_t, rollover_t) -> bool = delete;
constexpr friend auto operator>=(rollover_t, rollover_t) -> bool = delete;

[[nodiscard]] constexpr friend auto cmp_less(rollover_t lhs,
rollover_t rhs) -> bool {
constexpr auto mid = static_cast<underlying_t>(~underlying_t{}) / 2;
return static_cast<underlying_t>(lhs.value - rhs.value) > mid;
}

[[nodiscard]] constexpr friend auto
operator+(rollover_t lhs, rollover_t rhs) -> rollover_t {
lhs += rhs;
return lhs;
}
[[nodiscard]] constexpr friend auto
operator-(rollover_t lhs, rollover_t rhs) -> rollover_t {
lhs -= rhs;
return lhs;
}
[[nodiscard]] constexpr friend auto
operator*(rollover_t lhs, rollover_t rhs) -> rollover_t {
lhs *= rhs;
return lhs;
}
[[nodiscard]] constexpr friend auto
operator/(rollover_t lhs, rollover_t rhs) -> rollover_t {
lhs /= rhs;
return lhs;
}
[[nodiscard]] constexpr friend auto
operator%(rollover_t lhs, rollover_t rhs) -> rollover_t {
lhs %= rhs;
return lhs;
}

underlying_t value{};
};

template <typename T> rollover_t(T) -> rollover_t<T>;
} // namespace v1
} // namespace stdx

template <typename T, typename U>
struct std::common_type<stdx::rollover_t<T>, stdx::rollover_t<U>> {
using type = stdx::rollover_t<std::common_type_t<T, U>>;
};

template <typename T, typename I>
struct std::common_type<stdx::rollover_t<T>, I> {
using type =
stdx::rollover_t<std::common_type_t<T, std::make_unsigned_t<I>>>;
};
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ add_tests(
priority
ranges
remove_cvref
rollover
span
to_underlying
type_map
Expand Down
2 changes: 2 additions & 0 deletions test/fail/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ add_fail_tests(
for_each_n_args_bad_size
optional_without_tombstone
optional_integral_with_tombstone_traits
rollover_less_than
rollover_signed
span_insufficient_storage
span_larger_prefix
span_larger_suffix
Expand Down
8 changes: 8 additions & 0 deletions test/fail/rollover_less_than.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#include <stdx/rollover.hpp>

// EXPECT: deleted (operator|function)

auto main() -> int {
using X = stdx::rollover_t<unsigned int>;
[[maybe_unused]] auto cmp = X{} < X{1u};
}
8 changes: 8 additions & 0 deletions test/fail/rollover_signed.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#include <stdx/rollover.hpp>

// EXPECT: Argument to rollover_t must be an unsigned integral type

auto main() -> int {
using X = stdx::rollover_t<int>;
[[maybe_unused]] X x{};
}
Loading

0 comments on commit 9ebf17f

Please sign in to comment.