diff --git a/docs/index.adoc b/docs/index.adoc index 62fd055..6b2c496 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -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[] diff --git a/docs/intro.adoc b/docs/intro.adoc index 993f7d6..235683e 100644 --- a/docs/intro.adoc +++ b/docs/intro.adoc @@ -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`] diff --git a/docs/rollover.adoc b/docs/rollover.adoc new file mode 100644 index 0000000..1606995 --- /dev/null +++ b/docs/rollover.adoc @@ -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{}; + +// deduced type: must be unsigned +auto y = stdx::rollover_t{1u}; // rollover_t +---- + +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; +// Used with a microsecond resolution +using ro_duration_t = std::chrono::duration; +using ro_time_point_t = std::chrono::time_point; +---- + +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. diff --git a/include/stdx/rollover.hpp b/include/stdx/rollover.hpp new file mode 100644 index 0000000..f6f3585 --- /dev/null +++ b/include/stdx/rollover.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include + +#include + +namespace stdx { +inline namespace v1 { +template struct rollover_t { + static_assert(unsigned_integral, + "Argument to rollover_t must be an unsigned integral type."); + using underlying_t = T; + + constexpr rollover_t() = default; + template >> + constexpr explicit rollover_t(U u) : value{static_cast(u)} {} + template >> + constexpr explicit rollover_t(rollover_t u) + : rollover_t{static_cast(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(-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{}) / 2; + return static_cast(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 rollover_t(T) -> rollover_t; +} // namespace v1 +} // namespace stdx + +template +struct std::common_type, stdx::rollover_t> { + using type = stdx::rollover_t>; +}; + +template +struct std::common_type, I> { + using type = + stdx::rollover_t>>; +}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 08448aa..5d71cc8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -55,6 +55,7 @@ add_tests( priority ranges remove_cvref + rollover span to_underlying type_map diff --git a/test/fail/CMakeLists.txt b/test/fail/CMakeLists.txt index 4fcb2d2..00abcc0 100644 --- a/test/fail/CMakeLists.txt +++ b/test/fail/CMakeLists.txt @@ -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 diff --git a/test/fail/rollover_less_than.cpp b/test/fail/rollover_less_than.cpp new file mode 100644 index 0000000..6db0642 --- /dev/null +++ b/test/fail/rollover_less_than.cpp @@ -0,0 +1,8 @@ +#include + +// EXPECT: deleted (operator|function) + +auto main() -> int { + using X = stdx::rollover_t; + [[maybe_unused]] auto cmp = X{} < X{1u}; +} diff --git a/test/fail/rollover_signed.cpp b/test/fail/rollover_signed.cpp new file mode 100644 index 0000000..19ac818 --- /dev/null +++ b/test/fail/rollover_signed.cpp @@ -0,0 +1,8 @@ +#include + +// EXPECT: Argument to rollover_t must be an unsigned integral type + +auto main() -> int { + using X = stdx::rollover_t; + [[maybe_unused]] X x{}; +} diff --git a/test/rollover.cpp b/test/rollover.cpp new file mode 100644 index 0000000..a63d3d4 --- /dev/null +++ b/test/rollover.cpp @@ -0,0 +1,241 @@ +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +TEMPLATE_TEST_CASE("default construction", "[rollover]", std::uint8_t, + std::uint16_t, std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + constexpr rollover_t x; + static_assert(x == rollover_t{TestType{}}); + CHECK(x == rollover_t{TestType{}}); +} + +TEMPLATE_TEST_CASE("value construction", "[rollover]", std::uint8_t, + std::uint16_t, std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + constexpr rollover_t x{}; + static_assert(x == rollover_t{TestType{}}); + CHECK(x == rollover_t{TestType{}}); +} + +TEMPLATE_TEST_CASE("access to underlying value", "[rollover]", std::uint8_t, + std::uint16_t, std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + constexpr rollover_t x; + static_assert(x.as_underlying() == TestType{}); + CHECK(x.as_underlying() == TestType{}); +} + +TEMPLATE_TEST_CASE("cast to underlying type", "[rollover]", std::uint8_t, + std::uint16_t, std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + constexpr rollover_t x; + static_assert(static_cast(x) == + TestType{}); + CHECK(static_cast(x) == TestType{}); +} + +TEMPLATE_TEST_CASE("construction from convertible integral value", "[rollover]", + std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t) { + constexpr stdx::rollover_t x{std::uint8_t{16}}; + static_assert(x.as_underlying() == TestType{16}); + CHECK(x.as_underlying() == TestType{16}); +} + +TEMPLATE_TEST_CASE("construction from convertible rollover_t", "[rollover]", + std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t) { + constexpr stdx::rollover_t x{ + stdx::rollover_t{16}}; + static_assert(x.as_underlying() == TestType{16}); + CHECK(x.as_underlying() == TestType{16}); +} + +TEST_CASE("conversion with deduction guide", "[rollover]") { + constexpr auto x = stdx::rollover_t{16u}; + static_assert( + std::is_same_v const>); +} + +TEMPLATE_TEST_CASE("equality", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + rollover_t x{1}; + CHECK(x == rollover_t{1}); + CHECK(x != rollover_t{2}); +} + +TEMPLATE_TEST_CASE("unary plus", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + static_assert(+rollover_t{1} == rollover_t{1}); + CHECK(+rollover_t{1} == rollover_t{1}); +} + +TEMPLATE_TEST_CASE("unary minus", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + static_assert(-rollover_t{1} == rollover_t{-1}); + CHECK(-rollover_t{1} == rollover_t{-1}); +} + +TEMPLATE_TEST_CASE("increment", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + rollover_t x{1}; + CHECK(++x == rollover_t{2}); + CHECK(x++ == rollover_t{2}); + CHECK(x == rollover_t{3}); +} + +TEMPLATE_TEST_CASE("decrement", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + rollover_t x{3}; + CHECK(--x == rollover_t{2}); + CHECK(x-- == rollover_t{2}); + CHECK(x == rollover_t{1}); +} + +TEMPLATE_TEST_CASE("addition", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + rollover_t x{1}; + rollover_t y{1}; + x += y; + CHECK(x == rollover_t{2}); + CHECK(x + y == rollover_t{3}); +} + +TEMPLATE_TEST_CASE("subtraction", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + rollover_t x{2}; + rollover_t y{1}; + x -= y; + CHECK(x == rollover_t{1}); + CHECK(x + y == rollover_t{2}); +} + +TEMPLATE_TEST_CASE("multiplication", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + rollover_t x{2}; + rollover_t y{3}; + x *= y; + CHECK(x == rollover_t{6}); + CHECK(x * y == rollover_t{18}); +} + +TEMPLATE_TEST_CASE("division", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + rollover_t x{12}; + rollover_t y{3}; + x /= y; + CHECK(x == rollover_t{4}); + CHECK(rollover_t{12} / y == rollover_t{4}); +} + +TEMPLATE_TEST_CASE("mod operation", "[rollover]", std::uint8_t, std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + rollover_t x{13}; + rollover_t y{3}; + x %= y; + CHECK(x == rollover_t{1}); + CHECK(rollover_t{13} % y == rollover_t{1}); +} + +TEMPLATE_TEST_CASE("increment overflow", "[rollover]", std::uint8_t, + std::uint16_t, std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + auto x = rollover_t{std::numeric_limits::max()}; + ++x; + CHECK(x == rollover_t{}); +} + +TEMPLATE_TEST_CASE("decrement underflow", "[rollover]", std::uint8_t, + std::uint16_t, std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + auto x = rollover_t{}; + --x; + CHECK(x == rollover_t{std::numeric_limits::max()}); +} + +TEMPLATE_TEST_CASE("simple comparison", "[rollover]", std::uint8_t, + std::uint16_t, std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + CHECK(cmp_less(rollover_t{1}, rollover_t{2})); +} + +TEMPLATE_TEST_CASE("comparison at rollover point", "[rollover]", std::uint8_t, + std::uint16_t, std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + CHECK(cmp_less(rollover_t{std::numeric_limits::max()}, + rollover_t{})); + constexpr auto mid = + rollover_t{std::numeric_limits::max() / 2 + 1}; + CHECK(cmp_less(mid, rollover_t{})); + CHECK(cmp_less(rollover_t{}, mid)); +} + +TEMPLATE_TEST_CASE("exactly half the values are less", "[rollover]", + std::uint8_t) { + using rollover_t = stdx::rollover_t; + using count_t = std::uint64_t; + constexpr auto limit = count_t{std::numeric_limits::max()} + 1; + constexpr auto expected = limit / 2; + + auto x = std::array{}; + std::iota(std::begin(x), std::end(x), rollover_t{}); + + for (auto i : x) { + CHECK(std::count_if(std::begin(x), std::end(x), [&](auto val) { + return cmp_less(val, i); + }) == expected); + } +} + +TEMPLATE_TEST_CASE("chrono duration rep", "[rollover]", std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + using duration_sec_t = std::chrono::duration; + using duration_millisec_t = std::chrono::duration; + auto d1 = duration_sec_t{rollover_t{3u}}; + CHECK(d1.count() == rollover_t{3u}); + auto d2 = duration_millisec_t{d1}; + CHECK(d2.count() == rollover_t{3000u}); + auto d3 = std::chrono::duration_cast(d2); + CHECK(d3.count() == rollover_t{3u}); + CHECK((d1 == d2)); + CHECK((d2 == d3)); + CHECK((d2 - d1).count() == rollover_t{}); +} + +namespace { +struct local_clock {}; +} // namespace + +TEMPLATE_TEST_CASE("chrono time_point rep", "[rollover]", std::uint16_t, + std::uint32_t, std::uint64_t) { + using rollover_t = stdx::rollover_t; + using duration_sec_t = std::chrono::duration; + using duration_millisec_t = std::chrono::duration; + using tp_sec_t = std::chrono::time_point; + using tp_millisec_t = + std::chrono::time_point; + auto tp1 = tp_sec_t{duration_sec_t{rollover_t{3u}}}; + CHECK(tp1.time_since_epoch().count() == rollover_t{3u}); + auto tp2 = tp_millisec_t{tp1}; + CHECK(tp2.time_since_epoch().count() == rollover_t{3000u}); + CHECK((tp2 - tp1 == duration_sec_t{})); +}