From 0f66e24dba03bff9174f4afe6d811cc208985579 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:04:17 +0900 Subject: [PATCH 1/2] Add `iris::bitset` --- include/iris/bitset.hpp | 507 +++++++++++++++++++++++++++++++ include/iris/hash.hpp | 14 +- include/iris/stdint.hpp | 18 +- test/CMakeLists.txt | 1 + test/bitset.cpp | 647 ++++++++++++++++++++++++++++++++++++++++ test/stdint.cpp | 10 + 6 files changed, 1192 insertions(+), 5 deletions(-) create mode 100644 include/iris/bitset.hpp create mode 100644 test/bitset.cpp diff --git a/include/iris/bitset.hpp b/include/iris/bitset.hpp new file mode 100644 index 0000000..6d97ef0 --- /dev/null +++ b/include/iris/bitset.hpp @@ -0,0 +1,507 @@ +#ifndef IRIS_ZZ_BITSET_HPP +#define IRIS_ZZ_BITSET_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include +#include +#include + +#include +#include +#include +#include +#include // std::allocator +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // memset +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include + +namespace iris { + +namespace detail { + +template +[[nodiscard]] consteval auto bitset_default_word() noexcept +{ + if constexpr (N <= 8) { + return std::type_identity{}; + } else if constexpr (N <= 16) { + return std::type_identity{}; + } else if constexpr (N <= 32) { + return std::type_identity{}; + } else { + return std::type_identity{}; + } +} + +} // detail + +template())::type> +class bitset +{ + static_assert(iris::unsigned_numeric_integral); + + static constexpr std::ptrdiff_t bits_per_word = sizeof(T) * CHAR_BIT; + static constexpr std::ptrdiff_t words = N == 0 ? 0 : (N - 1) / bits_per_word; // -1 + + static constexpr T last_word_mask = N == 0 || N % bits_per_word != 0 + ? static_cast((T{1u} << (N % bits_per_word)) - 1) + : static_cast(~T{}); + +public: + static constexpr std::size_t bits = N; + using word_type = T; + + template + [[nodiscard]] static constexpr bitset from_positions(R&& positions) + { + return bitset::from_positions_impl(std::ranges::begin(positions), std::ranges::end(positions)); + } + + template + [[nodiscard]] static constexpr bitset from_positions(std::initializer_list positions) + { + return bitset::from_positions_impl(positions.begin(), positions.end()); + } + +private: + template Se> + [[nodiscard]] static constexpr bitset from_positions_impl(It first, Se last) + { + bitset tmp; + for (; first != last; ++first) { + tmp.set(static_cast(*first)); + } + return tmp; + } + +public: + constexpr bitset() noexcept = default; + + constexpr bitset(unsigned long long value) noexcept + { + constexpr std::ptrdiff_t ull_bits = sizeof(unsigned long long) * CHAR_BIT; + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + data_[word_pos] = static_cast(value); + if constexpr (bits_per_word < ull_bits) { + value >>= bits_per_word; + } else { + value = 0; + } + } + trim(); + } + + template + constexpr explicit bitset( + std::basic_string_view str, + std::size_t pos = 0, + std::size_t n = std::basic_string_view::npos, + CharT zero = CharT('0'), + CharT one = CharT('1') + ) + { + if (pos > str.size()) throwf("pos is out of range"); + str = str.substr(pos, n); + this->template assign_from_chars(str.data(), str.size(), zero, one); + } + + template + constexpr explicit bitset( + std::basic_string const& str, + std::size_t pos = 0, + std::size_t n = std::basic_string::npos, + CharT zero = CharT('0'), + CharT one = CharT('1') + ) + : bitset(std::basic_string_view(str), pos, n, zero, one) + {} + + template + constexpr explicit bitset( + CharT const* str, + std::size_t n = std::basic_string_view::npos, + CharT zero = CharT('0'), + CharT one = CharT('1') + ) + : bitset( + n == std::basic_string_view::npos + ? std::basic_string_view(str) + : std::basic_string_view(str, n), + 0, std::basic_string_view::npos, zero, one + ) + {} + + // -------------------------------------------------------- + + [[nodiscard]] constexpr std::span data() const noexcept + { + return data_; + } + + [[nodiscard]] constexpr bool operator[](std::size_t pos) const noexcept + { + return this->subscript(pos); + } + + [[nodiscard]] constexpr bool test(std::size_t pos) const + { + if (pos >= N) throwf("pos is out of range"); + return this->subscript(pos); + } + + [[nodiscard]] constexpr bool all() const noexcept + { + if constexpr (words > 0) { + for (std::ptrdiff_t word_pos = 0; word_pos < words; ++word_pos) { + if (data_[word_pos] != static_cast(~T{})) return false; + } + } + return data_[words] == last_word_mask; + } + + [[nodiscard]] constexpr bool any() const noexcept + { + return !this->none(); + } + + [[nodiscard]] constexpr bool none() const noexcept + { + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + if (data_[word_pos]) return false; + } + return true; + } + + // Returns the pop count + [[nodiscard]] constexpr std::size_t count() const noexcept + { + std::size_t c = 0; + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + c += std::popcount(data_[word_pos]); + } + return c; + } + + // Returns the number of bits that the bitset holds + [[nodiscard]] static constexpr std::size_t size() noexcept + { + return N; + } + + // ---------------------------------------------------- + + constexpr bitset& set() noexcept + { + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + data_[word_pos] = static_cast(~T{}); + } + trim(); + return *this; + } + + constexpr bitset& set(std::size_t pos, bool value = true) + { + if (pos >= N) throwf("pos is out of range"); + return this->set_unchecked(pos, value); + } + + constexpr bitset& reset() noexcept + { + if consteval { + std::ranges::fill(data_, T{}); + } else { + std::memset(&data_, 0, sizeof(data_)); + } + return *this; + } + + constexpr bitset& reset(std::size_t pos) + { + if (pos >= N) throwf("pos is out of range"); + return this->set_unchecked(pos, false); + } + + constexpr bitset& flip() noexcept + { + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + data_[word_pos] = static_cast(~data_[word_pos]); + } + trim(); + return *this; + } + + constexpr bitset& flip(std::size_t pos) + { + if (pos >= N) throwf("pos is out of range"); + data_[pos / bits_per_word] ^= bit_mask(pos); + return *this; + } + + // ---------------------------------------------------- + + constexpr bitset& operator&=(bitset const& other) noexcept + { + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + data_[word_pos] &= other.data_[word_pos]; + } + return *this; + } + + constexpr bitset& operator|=(bitset const& other) noexcept + { + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + data_[word_pos] |= other.data_[word_pos]; + } + return *this; + } + + constexpr bitset& operator^=(bitset const& other) noexcept + { + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + data_[word_pos] ^= other.data_[word_pos]; + } + return *this; + } + + // ---------------------------------------------------- + + [[nodiscard]] constexpr bitset operator~() const noexcept + { + bitset tmp = *this; + tmp.flip(); + return tmp; + } + + // ---------------------------------------------------- + + constexpr bitset& operator<<=(std::size_t pos) noexcept + { + auto const word_shift = static_cast(pos / bits_per_word); + if (word_shift != 0) { + for (std::ptrdiff_t word_pos = words; 0 <= word_pos; --word_pos) { + data_[word_pos] = word_shift <= word_pos ? data_[word_pos - word_shift] : 0; + } + } + if ((pos %= bits_per_word) != 0) { + if constexpr (words > 0) { + for (std::ptrdiff_t word_pos = words; 0 < word_pos; --word_pos) { + data_[word_pos] = (data_[word_pos] << pos) | (data_[word_pos - 1] >> (bits_per_word - pos)); + } + } + data_[0] <<= pos; + } + trim(); + return *this; + } + + [[nodiscard]] constexpr bitset operator<<(std::size_t pos) const noexcept + { + bitset tmp = *this; + tmp <<= pos; + return tmp; + } + + constexpr bitset& operator>>=(std::size_t pos) noexcept + { + auto const word_shift = static_cast(pos / bits_per_word); + if (word_shift != 0) { + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + data_[word_pos] = word_shift <= words - word_pos ? data_[word_pos + word_shift] : 0; + } + } + if ((pos %= bits_per_word) != 0) { + if constexpr (words > 0) { + for (std::ptrdiff_t word_pos = 0; word_pos < words; ++word_pos) { + data_[word_pos] = (data_[word_pos] >> pos) | (data_[word_pos + 1] << (bits_per_word - pos)); + } + } + data_[words] >>= pos; + } + return *this; + } + + [[nodiscard]] constexpr bitset operator>>(std::size_t pos) const noexcept + { + bitset tmp = *this; + tmp >>= pos; + return tmp; + } + + // ---------------------------------------------------- + + [[nodiscard]] constexpr bool operator==(bitset const&) const noexcept = default; + + [[nodiscard]] constexpr std::strong_ordering operator<=>(bitset const& other) const noexcept + { + for (std::ptrdiff_t word_pos = words; 0 <= word_pos; --word_pos) { + if (auto const c = data_[word_pos] <=> other.data_[word_pos]; c != 0) { + return c; + } + } + return std::strong_ordering::equal; + } + + // ---------------------------------------------------- + + [[nodiscard]] constexpr unsigned long to_ulong() const + { + return this->to_unsigned(); + } + + [[nodiscard]] constexpr unsigned long long to_ullong() const + { + return this->to_unsigned(); + } + + template, class Allocator = std::allocator> + [[nodiscard]] constexpr std::basic_string + to_string(CharT zero = CharT('0'), CharT one = CharT('1')) const + { + std::basic_string str(N, zero); + if constexpr (N != 0) { + for (std::size_t pos = 0; pos < N; ++pos) { + if ((*this)[pos]) { + str[N - 1 - pos] = one; + } + } + } + return str; + } + +private: + friend struct std::hash; + + [[nodiscard]] static constexpr T bit_mask(std::size_t pos) noexcept + { + return static_cast(T{1u} << (pos % bits_per_word)); + } + + [[nodiscard]] constexpr bool subscript(std::size_t pos) const noexcept + { + return (data_[pos / bits_per_word] & bitset::bit_mask(pos)) != 0; + } + + constexpr bitset& set_unchecked(std::size_t pos, bool value) noexcept + { + T& word = data_[pos / bits_per_word]; + if (value) { + word |= bitset::bit_mask(pos); + } else { + word &= static_cast(~bitset::bit_mask(pos)); + } + return *this; + } + + constexpr void trim() noexcept + { + if constexpr (last_word_mask != static_cast(~T{})) { + data_[words] &= last_word_mask; + } + } + + template + constexpr void assign_from_chars(CharT const* chars, std::size_t len, CharT zero, CharT one) + { + for (std::size_t i = 0; i < len; ++i) { + if (!Traits::eq(chars[i], zero) && !Traits::eq(chars[i], one)) { + throwf("character is neither zero nor one"); + } + } + std::size_t const m = std::min(N, len); + for (std::size_t i = 0; i < m; ++i) { + if (Traits::eq(chars[m - 1 - i], one)) { + this->set_unchecked(i, true); + } + } + } + + // std::overflow_error if any set bit does not fit into U. + template + [[nodiscard]] constexpr U to_unsigned() const + { + constexpr std::ptrdiff_t u_bits = sizeof(U) * CHAR_BIT; + U result = 0; + for (std::ptrdiff_t word_pos = 0; word_pos <= words; ++word_pos) { + std::ptrdiff_t const shift = word_pos * bits_per_word; + if (shift >= u_bits) { + if (data_[word_pos] != 0) { + throwf("bitset does not fit into the target type"); + } + continue; + } + if (shift + bits_per_word > u_bits) { + if ((data_[word_pos] >> (u_bits - shift)) != 0) { + throwf("bitset does not fit into the target type"); + } + } + result |= static_cast(static_cast(data_[word_pos]) << shift); + } + return result; + } + + T data_[words + 1]{}; +}; + +template +[[nodiscard]] constexpr bitset operator&(bitset const& a, bitset const& b) noexcept +{ + bitset tmp = a; + tmp &= b; + return tmp; +} + +template +[[nodiscard]] constexpr bitset operator|(bitset const& a, bitset const& b) noexcept +{ + bitset tmp = a; + tmp |= b; + return tmp; +} + +template +[[nodiscard]] constexpr bitset operator^(bitset const& a, bitset const& b) noexcept +{ + bitset tmp = a; + tmp ^= b; + return tmp; +} + +} // iris + +template +struct std::hash> +{ + [[nodiscard]] static std::size_t operator()(iris::bitset const& bs) noexcept + { + return iris::hash_value(bs.data_); + } +}; + +template +struct std::formatter, CharT> + : std::formatter, CharT> +{ + template + FormatContext::iterator format(iris::bitset const& bs, FormatContext& ctx) const + { + return std::formatter, CharT>::format(bs.template to_string(), ctx); + } +}; + +#endif diff --git a/include/iris/hash.hpp b/include/iris/hash.hpp index 87aa53a..30ce008 100644 --- a/include/iris/hash.hpp +++ b/include/iris/hash.hpp @@ -10,6 +10,7 @@ #include #include +#include #include @@ -123,16 +124,21 @@ template return std::hash{}(var); } -template +template + requires std::ranges::input_range [[nodiscard]] constexpr std::size_t hash_value(R const& r) noexcept( - noexcept(++std::ranges::begin(r)) && + noexcept(std::ranges::begin(r)) && noexcept(std::ranges::end(r)) && - std::is_nothrow_copy_assignable_v> + noexcept(++std::declval&>()) && + noexcept(std::declval&>() != std::declval&>()) && + noexcept(iris::hash_value(*std::declval&>())) ) { std::size_t seed = 0; - for (auto it = std::ranges::begin(r), se = std::ranges::end(r); it != se; ++it) { + auto it = std::ranges::begin(r); + auto const se = std::ranges::end(r); + for (; it != se; ++it) { seed = iris::hash_combine(seed, iris::hash_value(*it)); } return seed; diff --git a/include/iris/stdint.hpp b/include/iris/stdint.hpp index 2d32189..5065513 100644 --- a/include/iris/stdint.hpp +++ b/include/iris/stdint.hpp @@ -20,7 +20,7 @@ namespace iris { namespace detail { template -struct integer_of_size_impl; +struct integer_of_size_impl; // undefined template<> struct integer_of_size_impl @@ -125,6 +125,22 @@ struct make_integer_of_size template using make_integer_of_size_t = make_integer_of_size::type; +// ------------------------------------------- + +template +struct signed_integer_of_size : detail::integer_of_size_impl +{}; + +template +using signed_integer_of_size_t = signed_integer_of_size::type; + +template +struct unsigned_integer_of_size : detail::integer_of_size_impl +{}; + +template +using unsigned_integer_of_size_t = unsigned_integer_of_size::type; + } // iris #endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ead5c33..136577c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -199,6 +199,7 @@ if(PROJECT_IS_TOP_LEVEL) marshal indexed_value run_length_sequence + bitset ) foreach(test_name IN LISTS IRIS_TEST_IRIS_TESTS) iris_define_internal_test(${test_name} ${test_name}.cpp) diff --git a/test/bitset.cpp b/test/bitset.cpp new file mode 100644 index 0000000..a44a85f --- /dev/null +++ b/test/bitset.cpp @@ -0,0 +1,647 @@ +#include "iris_test.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // IWYU pragma: keep +#include + +namespace { + +template +struct bitset_test_cfg +{ + static constexpr std::size_t n = N; + using word = T; +}; + +#define IRIS_TEST_BITSET_CFGS \ + (bitset_test_cfg<1, std::uint8_t>), (bitset_test_cfg<7, std::uint8_t>), (bitset_test_cfg<8, std::uint8_t>), \ + (bitset_test_cfg<9, std::uint8_t>), (bitset_test_cfg<16, std::uint8_t>), (bitset_test_cfg<37, std::uint8_t>), \ + (bitset_test_cfg<37, std::uint16_t>), (bitset_test_cfg<64, std::uint8_t>), (bitset_test_cfg<64, std::uint64_t>), \ + (bitset_test_cfg<65, std::uint64_t>), (bitset_test_cfg<100, std::uint8_t>), (bitset_test_cfg<130, std::uint64_t>) + +constexpr std::array patterns{ + 0ULL, + 1ULL, + 0x80ULL, + 0xFFULL, + 0x0100ULL, + 0x1234'5678'9ABC'DEF0ULL, + 0x8000'0000'0000'0000ULL, + 0xAAAA'AAAA'AAAA'AAAAULL, + 0xFFFF'FFFF'FFFF'FFFFULL, +}; + +template +std::string as_std_string(iris::bitset const& b) +{ + return b.to_string(); +} + +template +std::string as_std_string(std::bitset const& b) +{ + return b.to_string(); +} + +enum class Flag : unsigned { a = 0, b = 3, c = 7 }; + +} // anonymous + +// ---------------------------------------------------- +// Static properties + +TEST_CASE("type traits", "[bitset]") +{ + STATIC_REQUIRE(std::same_as::word_type, std::uint_least8_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least8_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least8_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least16_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least16_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least32_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least32_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least64_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least64_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least64_t>); + STATIC_REQUIRE(std::same_as::word_type, std::uint_least64_t>); + + STATIC_REQUIRE(sizeof(iris::bitset<0>) == 1); + STATIC_REQUIRE(sizeof(iris::bitset<8>) == 1); + STATIC_REQUIRE(sizeof(iris::bitset<9>) == 2); + STATIC_REQUIRE(sizeof(iris::bitset<64>) == 8); + STATIC_REQUIRE(sizeof(iris::bitset<65>) == 16); + STATIC_REQUIRE(sizeof(iris::bitset<64, std::uint8_t>) == 8); + STATIC_REQUIRE(alignof(iris::bitset<64, std::uint8_t>) == 1); + STATIC_REQUIRE(sizeof(iris::bitset<100, std::uint8_t>) == 13); + + STATIC_REQUIRE(std::is_convertible_v>); + STATIC_REQUIRE(std::is_convertible_v>); + STATIC_REQUIRE(std::is_constructible_v, char const*>); + STATIC_REQUIRE(!std::is_convertible_v>); + STATIC_REQUIRE(std::is_constructible_v, std::string>); + STATIC_REQUIRE(!std::is_convertible_v>); + STATIC_REQUIRE(std::is_constructible_v, std::string_view>); + STATIC_REQUIRE(!std::is_convertible_v>); + + STATIC_REQUIRE(std::is_trivially_copyable_v>); + STATIC_REQUIRE(std::is_trivially_copyable_v>); + STATIC_REQUIRE(std::is_standard_layout_v>); +} + +// ---------------------------------------------------- +// Construction + +TEMPLATE_TEST_CASE("construction", "[bitset]", IRIS_TEST_BITSET_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + { + B const b; + CHECK(b.none()); + CHECK(!b.any()); + CHECK(b.count() == 0); + CHECK(b.size() == N); + CHECK(B::bits == N); + CHECK(as_std_string(b) == as_std_string(std::bitset{})); + CHECK(b == B{}); + } + + for (auto const v : patterns) { + B const b(v); + std::bitset const s(v); + INFO("value = " << v); + CHECK(as_std_string(b) == as_std_string(s)); + CHECK(b.count() == s.count()); + CHECK(b.to_ullong() == s.to_ullong()); + } + + SECTION("string_view") + { + CHECK(iris::bitset<8>(std::string_view{"1010"}).to_ulong() == 0b1010); + CHECK(iris::bitset<8>(std::string_view{"0110"}).to_string() == "00000110"); + CHECK(iris::bitset<4>(std::string_view{"11110000"}).to_ulong() == 0b1111); + CHECK(iris::bitset<4>(std::string_view{"11110000"}).to_string() == std::bitset<4>("11110000").to_string()); + CHECK(iris::bitset<8>(std::string_view{"xx1010"}, 2).to_ulong() == 0b1010); + CHECK(iris::bitset<8>(std::string_view{"xx1010yy"}, 2, 4).to_ulong() == 0b1010); + CHECK(iris::bitset<8>(std::string_view{"abab"}, 0, std::string_view::npos, 'a', 'b').to_ulong() == 0b0101); + CHECK(iris::bitset<8>(std::string_view{"1010"}, 4).none()); + } + SECTION("basic_string") + { + std::string const str = "xx1010yy"; + CHECK(iris::bitset<8>(str, 2, 4).to_ulong() == std::bitset<8>(str, 2, 4).to_ulong()); + CHECK_THROWS_AS(iris::bitset<8>(str, 2), std::invalid_argument); + CHECK(iris::bitset<8>(std::string{"xx1010"}, 2).to_string() == std::bitset<8>(std::string{"xx1010"}, 2).to_string()); + CHECK(iris::bitset<8>(std::string{"ab"}, 0, 2, 'a', 'b').to_ulong() == 0b01); + } + SECTION("CharT const*") + { + CHECK(iris::bitset<8>("1010").to_ulong() == std::bitset<8>("1010").to_ulong()); + CHECK(iris::bitset<8>("101011", 4).to_ulong() == std::bitset<8>("101011", 4).to_ulong()); + CHECK(iris::bitset<8>("ab", 2, 'a', 'b').to_ulong() == 0b01); + CHECK(iris::bitset<8>("ab", std::string_view::npos, 'a', 'b').to_ulong() == 0b01); + } + SECTION("wide characters") + { + CHECK(iris::bitset<8>(L"1010").to_ulong() == 0b1010); + CHECK(iris::bitset<8>(std::wstring_view{L"1010"}).to_string() == L"00001010"); + } + SECTION("exceptions") + { + CHECK_THROWS_AS(iris::bitset<8>("012"), std::invalid_argument); + CHECK_THROWS_AS(iris::bitset<8>("1010", 3, 'a', 'b'), std::invalid_argument); + CHECK_THROWS_AS(iris::bitset<8>("01", 3), std::invalid_argument); + CHECK_THROWS_AS(iris::bitset<8>(std::string{"01"}, 3), std::out_of_range); + CHECK_THROWS_AS(iris::bitset<8>(std::string_view{"01"}, 3), std::out_of_range); + CHECK_THROWS_AS(iris::bitset<2>("11x"), std::invalid_argument); + } +} + +TEST_CASE("from_positions", "[bitset]") +{ + using B = iris::bitset<16, std::uint8_t>; + + SECTION("initializer_list") + { + CHECK(B::from_positions({0, 9, 15}).to_string() == "1000001000000001"); + CHECK(B::from_positions({3, 3, 3}).count() == 1); + } + SECTION("range") + { + std::vector const v{1, 2, 4}; + CHECK(B::from_positions(v).to_ulong() == 0b10110); + CHECK(B::from_positions(std::array{15}).to_ulong() == 0x8000); + } + SECTION("scoped enum elements") + { + CHECK(B::from_positions({Flag::a, Flag::c}).to_ulong() == 0b1000'0001); + CHECK(B::from_positions(std::array{Flag::b}).to_ulong() == 0b1000); + } + SECTION("out of range") + { + CHECK_THROWS_AS(B::from_positions({16}), std::out_of_range); + CHECK_THROWS_AS(B::from_positions(std::vector{0, 100}), std::out_of_range); + CHECK_THROWS_AS(iris::bitset<0>::from_positions({0}), std::out_of_range); + } +} + +// ---------------------------------------------------- +// Element access and single-bit modifiers + +TEMPLATE_TEST_CASE("single-bit operations", "[bitset]", IRIS_TEST_BITSET_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + { + B b; + std::bitset s; + + // Walk every position with a deterministic mix of set / reset / flip. + for (std::size_t pos = 0; pos < N; ++pos) { + switch (pos % 4) { + case 0: + b.set(pos); + s.set(pos); + break; + case 1: + b.set(pos, true); + s.set(pos, true); + break; + case 2: + b.flip(pos); + s.flip(pos); + break; + default: + b.set(pos, false); + s.set(pos, false); + break; + } + REQUIRE(as_std_string(b) == as_std_string(s)); + } + + for (std::size_t pos = 0; pos < N; ++pos) { + REQUIRE(b[pos] == s[pos]); + REQUIRE(b.test(pos) == s.test(pos)); + } + + for (std::size_t pos = 0; pos < N; pos += 3) { + b.reset(pos); + s.reset(pos); + } + CHECK(as_std_string(b) == as_std_string(s)); + CHECK(b.count() == s.count()); + } + { + B b; + CHECK_THROWS_AS(b.set(N), std::out_of_range); + CHECK_THROWS_AS(b.set(N, false), std::out_of_range); + CHECK_THROWS_AS(b.reset(N), std::out_of_range); + CHECK_THROWS_AS(b.flip(N), std::out_of_range); + CHECK_THROWS_AS((void)b.test(N), std::out_of_range); + CHECK_THROWS_AS(b.set(static_cast(-1)), std::out_of_range); + CHECK(b.none()); + } +} + +TEST_CASE("modifier chaining", "[bitset]") +{ + iris::bitset<8> b; + CHECK(b.set().reset(0).flip(7).to_ulong() == 0b0111'1110); + CHECK(b.reset().set(1).to_ulong() == 0b10); + CHECK(b.flip().to_ulong() == 0b1111'1101); + STATIC_REQUIRE(std::same_as&>); + STATIC_REQUIRE(std::same_as&>); + STATIC_REQUIRE(std::same_as&>); +} + +// ---------------------------------------------------- +// Modifiers and acccessors + +TEMPLATE_TEST_CASE("set/reset/flip on all bits", "[bitset]", IRIS_TEST_BITSET_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + for (auto const v : patterns) { + B b(v); + std::bitset s(v); + INFO("value = " << v); + + b.flip(); + s.flip(); + CHECK(as_std_string(b) == as_std_string(s)); + CHECK(as_std_string(~b) == as_std_string(~s)); + + b.set(); + s.set(); + CHECK(as_std_string(b) == as_std_string(s)); + CHECK(b.all()); + CHECK(b.count() == N); + + b.reset(); + s.reset(); + CHECK(as_std_string(b) == as_std_string(s)); + CHECK(b.none()); + } +} + +TEMPLATE_TEST_CASE("all/any/none/count", "[bitset]", IRIS_TEST_BITSET_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + for (auto const v : patterns) { + B const b(v); + std::bitset const s(v); + INFO("value = " << v); + CHECK(b.all() == s.all()); + CHECK(b.any() == s.any()); + CHECK(b.none() == s.none()); + CHECK(b.count() == s.count()); + } + + // Partially used last word: all() must look only at the valid bits. + B full; + full.set(); + CHECK(full.all()); + if constexpr (N > 0) { + full.reset(N - 1); + CHECK(!full.all()); + CHECK(full.any() == (N > 1)); + } +} + +TEST_CASE("N == 0", "[bitset]") +{ + using B = iris::bitset<0>; + B b; + CHECK(b.size() == 0); + CHECK(b.count() == 0); + CHECK(b.all()); + CHECK(b.none()); + CHECK(!b.any()); + CHECK(b.to_ullong() == 0); + CHECK(b.to_string().empty()); + CHECK(b.flip().count() == 0); // padding must not become visible + CHECK(b.set().count() == 0); + CHECK(b.all()); + CHECK((b << 5) == B{}); + CHECK((b >> 5) == B{}); + CHECK(b == B{}); + CHECK((b <=> B{}) == std::strong_ordering::equal); + CHECK(B(0xFF).none()); + CHECK_THROWS_AS(b.set(0), std::out_of_range); + CHECK_THROWS_AS((void)b.test(0), std::out_of_range); + CHECK(std::format("[{}]", b) == "[]"); + CHECK(std::hash{}(b) == std::hash{}(B{})); +} + +// ---------------------------------------------------- +// Bitwise operators + +TEMPLATE_TEST_CASE("bitwise operators", "[bitset]", IRIS_TEST_BITSET_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + for (auto const x : patterns) { + for (auto const y : patterns) { + B const bx(x); + B const by(y); + std::bitset const sx(x); + std::bitset const sy(y); + INFO("x = " << x << ", y = " << y); + + CHECK(as_std_string(bx & by) == as_std_string(sx & sy)); + CHECK(as_std_string(bx | by) == as_std_string(sx | sy)); + CHECK(as_std_string(bx ^ by) == as_std_string(sx ^ sy)); + + B b = bx; + std::bitset s = sx; + b &= by; + s &= sy; + CHECK(as_std_string(b) == as_std_string(s)); + + b = bx; + s = sx; + b |= by; + s |= sy; + CHECK(as_std_string(b) == as_std_string(s)); + + b = bx; + s = sx; + b ^= by; + s ^= sy; + CHECK(as_std_string(b) == as_std_string(s)); + } + } +} + +TEMPLATE_TEST_CASE("shifts", "[bitset]", IRIS_TEST_BITSET_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + std::vector shifts; + for (std::size_t i = 0; i <= N + 2; ++i) { + shifts.push_back(i); + } + shifts.push_back(200); + shifts.push_back(1000); + + for (auto const v : patterns) { + B const b(v); + std::bitset const s(v); + for (auto const sh : shifts) { + INFO("value = " << v << ", shift = " << sh); + REQUIRE(as_std_string(b << sh) == as_std_string(s << sh)); + REQUIRE(as_std_string(b >> sh) == as_std_string(s >> sh)); + + B bl = b; + std::bitset sl = s; + bl <<= sh; + sl <<= sh; + REQUIRE(as_std_string(bl) == as_std_string(sl)); + + B br = b; + std::bitset sr = s; + br >>= sh; + sr >>= sh; + REQUIRE(as_std_string(br) == as_std_string(sr)); + } + } +} + +TEST_CASE("shifted-out bits never leak into padding", "[bitset]") +{ + iris::bitset<12, std::uint8_t> b(0xFFF); + b <<= 4; + CHECK(b.to_ulong() == 0xFF0); + CHECK(b.count() == 8); + CHECK((~b).to_ulong() == 0x00F); + CHECK(b == iris::bitset<12, std::uint8_t>(0xFF0)); + CHECK(std::hash>{}(b) == std::hash>{}(iris::bitset<12, std::uint8_t>(0xFF0))); +} + +// ---------------------------------------------------- +// Comparison + +TEMPLATE_TEST_CASE("equality", "[bitset]", IRIS_TEST_BITSET_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + for (auto const x : patterns) { + for (auto const y : patterns) { + INFO("x = " << x << ", y = " << y); + CHECK((B(x) == B(y)) == (std::bitset(x) == std::bitset(y))); + CHECK((B(x) != B(y)) == (std::bitset(x) != std::bitset(y))); + } + } +} + +TEST_CASE("ordering", "[bitset]") +{ + using B8 = iris::bitset<16, std::uint8_t>; + using B16 = iris::bitset<16, std::uint16_t>; + using B64 = iris::bitset<16, std::uint64_t>; + + CHECK(B8(0x0100) > B8(0x00FF)); + CHECK(B16(0x0100) > B16(0x00FF)); + CHECK(B64(0x0100) > B64(0x00FF)); + + std::vector const values{0, 0xFF, 0x0100, 0x0101, 0x8000, 0xFFFF, 0x00FE, 0x7FFF}; + std::vector sorted_by_value = values; + std::ranges::sort(sorted_by_value); + + auto const check = [&](std::type_identity) { + std::vector sorted; + for (auto const v : values) { + sorted.emplace_back(v); // NOLINT(performance-inefficient-vector-operation) + } + std::ranges::sort(sorted); + for (std::size_t i = 0; i < sorted.size(); ++i) { + REQUIRE(sorted[i].to_ulong() == sorted_by_value[i]); + } + }; + check(std::type_identity{}); + check(std::type_identity{}); + check(std::type_identity{}); + + using W = iris::bitset<100, std::uint8_t>; + W hi; + hi.set(99); + W lo; + lo.set(); + lo.reset(99); + CHECK(hi > lo); + CHECK((hi <=> lo) == std::strong_ordering::greater); + CHECK((lo <=> hi) == std::strong_ordering::less); + CHECK((hi <=> hi) == std::strong_ordering::equal); + CHECK(std::set{lo, hi, W{}}.size() == 3); +} + +// ---------------------------------------------------- +// Conversions + +TEMPLATE_TEST_CASE("to_ulong/to_ullong", "[bitset]", IRIS_TEST_BITSET_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + for (auto const v : patterns) { + B const b(v); + std::bitset const s(v); + INFO("value = " << v); + CHECK(b.to_ullong() == s.to_ullong()); + + bool const fits_ulong = s.to_ullong() <= static_cast(~0UL); // NOLINT(bugprone-misplaced-widening-cast) + if (fits_ulong) { + CHECK(b.to_ulong() == s.to_ulong()); + } else { + CHECK_THROWS_AS((void)b.to_ulong(), std::overflow_error); + CHECK_THROWS_AS((void)s.to_ulong(), std::overflow_error); + } + } + + { + iris::bitset<70> b; + b.set(64); + CHECK_THROWS_AS((void)b.to_ullong(), std::overflow_error); + b.reset(64); + b.set(63); + CHECK(b.to_ullong() == 0x8000'0000'0000'0000ULL); + + iris::bitset<130, std::uint8_t> w; + w.set(129); + CHECK_THROWS_AS((void)w.to_ullong(), std::overflow_error); + w.reset(129); + w.set(0); + CHECK(w.to_ullong() == 1); + + if constexpr (sizeof(unsigned long) == 4) { + iris::bitset<64, std::uint64_t> x; + x.set(32); + CHECK_THROWS_AS((void)x.to_ulong(), std::overflow_error); + x.reset(32); + x.set(31); + CHECK(x.to_ulong() == 0x8000'0000UL); + } + } +} + +TEST_CASE("to_string", "[bitset]") +{ + iris::bitset<8> const b(0b0010'1101); + CHECK(b.to_string() == "00101101"); + CHECK(b.to_string() == std::bitset<8>(0b0010'1101).to_string()); + CHECK(b.to_string('.', '#') == "..#.##.#"); + CHECK(b.to_string() == L"00101101"); + CHECK(b.to_string(L'_', L'X') == L"__X_XX_X"); + CHECK(iris::bitset<130, std::uint8_t>{}.set(129).to_string() == std::bitset<130>{}.set(129).to_string()); +} + +TEST_CASE("data()", "[bitset]") +{ + iris::bitset<20, std::uint8_t> const b(0x0A0B0C); + auto const d = b.data(); + STATIC_REQUIRE(std::same_as const>); + CHECK(d[0] == 0x0C); + CHECK(d[1] == 0x0B); + CHECK(d[2] == 0x0A); + + iris::bitset<0> const z; + CHECK(z.data().size() == 1); + CHECK(z.data()[0] == 0); +} + +// ---------------------------------------------------- +// Hashing and formatting + +TEST_CASE("std::hash", "[bitset]") +{ + using B = iris::bitset<100, std::uint8_t>; + std::hash const h; + + CHECK(h(B(5)) == h(B(5))); + CHECK(h(B{}.set(99)) == h(B{}.set(99))); + + std::unordered_set set; + set.insert(B(1)); + set.insert(B(1)); + set.insert(B(2)); + CHECK(set.size() == 2); + CHECK(set.contains(B(1))); + CHECK(!set.contains(B(3))); +} + +TEST_CASE("std::formatter", "[bitset]") +{ + iris::bitset<8> const b(5); + CHECK(std::format("{}", b) == "00000101"); + CHECK(std::format("{}", b) == std::bitset<8>(5).to_string()); + CHECK(std::format("{:>12}", b) == " 00000101"); + CHECK(std::format("{:<12}|", b) == "00000101 |"); + CHECK(std::format("{:*^12}", b) == "**00000101**"); + CHECK(std::format("{:.4}", b) == "0000"); + CHECK(std::format(L"{}", iris::bitset<4>(0b1010)) == L"1010"); + CHECK(std::format("{}", iris::bitset<130, std::uint8_t>{}.set(129)) == std::bitset<130>{}.set(129).to_string()); +} + +// ---------------------------------------------------- +// Constant evaluation + +TEST_CASE("usable in constant expressions", "[bitset]") +{ + using B = iris::bitset<12, std::uint8_t>; + + STATIC_REQUIRE(B(0xABC).to_ulong() == 0xABC); + STATIC_REQUIRE(B("101").to_ulong() == 0b101); + STATIC_REQUIRE(B(std::string_view{"xx101"}, 2).to_ulong() == 0b101); + STATIC_REQUIRE(B::from_positions({0, 11}).to_ulong() == 0x801); + STATIC_REQUIRE(B{}.set().count() == 12); + STATIC_REQUIRE(B{}.set(3).flip(3).none()); + STATIC_REQUIRE(B{}.set().reset(11).to_ulong() == 0x7FF); + STATIC_REQUIRE((B(0xF0F) & B(0x0FF)) == B(0x00F)); + STATIC_REQUIRE((B(0xF0F) | B(0x0F0)) == B(0xFFF)); + STATIC_REQUIRE((B(0xF0F) ^ B(0xFFF)) == B(0x0F0)); + STATIC_REQUIRE((~B(0xF0F)) == B(0x0F0)); + STATIC_REQUIRE((B(1) << 11) == B(0x800)); + STATIC_REQUIRE((B(1) << 12).none()); + STATIC_REQUIRE((B(0x800) >> 11) == B(1)); + STATIC_REQUIRE(B(0x100) > B(0x0FF)); + STATIC_REQUIRE(B(0x0FF).all() == false); + STATIC_REQUIRE(B(0xFFF).all()); + STATIC_REQUIRE(B(0xFFF).any()); + STATIC_REQUIRE(B(0).none()); + STATIC_REQUIRE(B(0xABC).to_string() == "101010111100"); + STATIC_REQUIRE(B(0xABC)[2] && !B(0xABC)[0]); + STATIC_REQUIRE(B(0xABC).test(2)); + STATIC_REQUIRE(B(0xABC).data()[1] == 0x0A); + + using W = iris::bitset<130, std::uint64_t>; + STATIC_REQUIRE(W{}.set(129).count() == 1); + STATIC_REQUIRE((W{}.set(129) >> 129) == W(1)); + STATIC_REQUIRE((W(1) << 129).test(129)); + STATIC_REQUIRE(W{}.set().count() == 130); + STATIC_REQUIRE(W{}.set(129) > W{}.set(128)); +} + +#undef IRIS_TEST_BITSET_CFGS diff --git a/test/stdint.cpp b/test/stdint.cpp index 44952e6..5daa51e 100644 --- a/test/stdint.cpp +++ b/test/stdint.cpp @@ -115,4 +115,14 @@ TEST_CASE("stdint") STATIC_CHECK(std::same_as, std::int16_t>); STATIC_CHECK(std::same_as, std::int16_t const>); } + + STATIC_CHECK(std::same_as, std::int8_t>); + STATIC_CHECK(std::same_as, std::int16_t>); + STATIC_CHECK(std::same_as, std::int32_t>); + STATIC_CHECK(std::same_as, std::int64_t>); + + STATIC_CHECK(std::same_as, std::uint8_t>); + STATIC_CHECK(std::same_as, std::uint16_t>); + STATIC_CHECK(std::same_as, std::uint32_t>); + STATIC_CHECK(std::same_as, std::uint64_t>); } From c15732edf653e25313cd01d750c093947c88de38 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:30:50 +0900 Subject: [PATCH 2/2] Add `positions()` --- include/iris/bitset.hpp | 158 +++++++++++++++++++++++++++++++++++++--- test/bitset.cpp | 63 ++++++++++++++++ 2 files changed, 212 insertions(+), 9 deletions(-) diff --git a/include/iris/bitset.hpp b/include/iris/bitset.hpp index 6d97ef0..7882b9b 100644 --- a/include/iris/bitset.hpp +++ b/include/iris/bitset.hpp @@ -35,6 +35,9 @@ namespace iris { namespace detail { +template +class bitset_positions_view; + template [[nodiscard]] consteval auto bitset_default_word() noexcept { @@ -56,6 +59,8 @@ class bitset { static_assert(iris::unsigned_numeric_integral); + friend class detail::bitset_positions_view; + static constexpr std::ptrdiff_t bits_per_word = sizeof(T) * CHAR_BIT; static constexpr std::ptrdiff_t words = N == 0 ? 0 : (N - 1) / bits_per_word; // -1 @@ -83,11 +88,19 @@ class bitset template Se> [[nodiscard]] static constexpr bitset from_positions_impl(It first, Se last) { - bitset tmp; - for (; first != last; ++first) { - tmp.set(static_cast(*first)); + if constexpr (N == 0) { + if (first != last) { + throwf("you cannot construct a 0-bit bitset from a non-empty range"); + } + return bitset{}; + + } else { + bitset tmp; + for (; first != last; ++first) { + tmp.set(static_cast(*first)); + } + return tmp; } - return tmp; } public: @@ -149,11 +162,6 @@ class bitset // -------------------------------------------------------- - [[nodiscard]] constexpr std::span data() const noexcept - { - return data_; - } - [[nodiscard]] constexpr bool operator[](std::size_t pos) const noexcept { return this->subscript(pos); @@ -384,6 +392,15 @@ class bitset return str; } + // --------------------------------------------------- + + [[nodiscard]] constexpr std::span data() const noexcept + { + return data_; + } + + [[nodiscard]] constexpr detail::bitset_positions_view positions() const noexcept; + private: friend struct std::hash; @@ -482,6 +499,129 @@ template return tmp; } +namespace detail { + +template +class bitset_positions_view> + : public std::ranges::view_interface>> +{ +public: + class iterator + { + public: + using iterator_concept = std::bidirectional_iterator_tag; + using iterator_category = std::bidirectional_iterator_tag; + using value_type = std::size_t; + using difference_type = std::ptrdiff_t; + + constexpr iterator() noexcept = default; + + [[nodiscard]] constexpr std::size_t operator*() const noexcept + { + return static_cast(word_pos_) * bitset::bits_per_word + static_cast(std::countr_zero(cur_)); + } + + constexpr iterator& operator++() noexcept + { + cur_ &= static_cast(cur_ - 1); // clear the lowest set bit + this->skip_zero_words(); + return *this; + } + + constexpr iterator operator++(int) noexcept + { + iterator tmp = *this; + ++*this; + return tmp; + } + + constexpr iterator& operator--() noexcept + { + T below = 0; + if (word_pos_ <= bitset::words) { + below = static_cast(data_[word_pos_] ^ cur_); + } + while (below == 0) { + --word_pos_; + below = data_[word_pos_]; + cur_ = 0; + } + cur_ |= static_cast(T{1u} << (std::bit_width(below) - 1)); + return *this; + } + + constexpr iterator operator--(int) noexcept + { + iterator tmp = *this; + --*this; + return tmp; + } + + [[nodiscard]] constexpr bool operator==(iterator const&) const noexcept = default; + + private: + friend bitset_positions_view; + + struct end_tag {}; + + constexpr explicit iterator(T const* data) noexcept + : data_(data) + , word_pos_(0) + , cur_(data[0]) + { + this->skip_zero_words(); + } + + constexpr iterator(T const* data, end_tag) noexcept + : data_(data) + {} + + constexpr void skip_zero_words() noexcept + { + while (cur_ == 0) { + if (++word_pos_ > bitset::words) return; + cur_ = data_[word_pos_]; + } + } + + T const* data_ = nullptr; + std::ptrdiff_t word_pos_ = bitset::words + 1; + T cur_ = 0; + }; + + constexpr bitset_positions_view() noexcept = default; + + constexpr explicit bitset_positions_view(bitset const& bs) noexcept + : bs_(bs) + {} + + [[nodiscard]] constexpr iterator begin() const noexcept + { + return iterator(bs_.data_); + } + + [[nodiscard]] constexpr iterator end() const noexcept + { + return iterator(bs_.data_, typename iterator::end_tag{}); + } + + [[nodiscard]] constexpr std::size_t size() const noexcept + { + return bs_.count(); + } + +private: + bitset bs_; +}; + +} // detail + +template +constexpr detail::bitset_positions_view> bitset::positions() const noexcept +{ + return detail::bitset_positions_view>{*this}; +} + } // iris template diff --git a/test/bitset.cpp b/test/bitset.cpp index a44a85f..439669b 100644 --- a/test/bitset.cpp +++ b/test/bitset.cpp @@ -35,6 +35,11 @@ struct bitset_test_cfg (bitset_test_cfg<37, std::uint16_t>), (bitset_test_cfg<64, std::uint8_t>), (bitset_test_cfg<64, std::uint64_t>), \ (bitset_test_cfg<65, std::uint64_t>), (bitset_test_cfg<100, std::uint8_t>), (bitset_test_cfg<130, std::uint64_t>) +#define IRIS_TEST_BITSET_POSITIONS_CFGS \ + (bitset_test_cfg<0, std::uint8_t>), (bitset_test_cfg<1, std::uint8_t>), (bitset_test_cfg<8, std::uint8_t>), (bitset_test_cfg<37, std::uint16_t>), \ + (bitset_test_cfg<64, std::uint8_t>), (bitset_test_cfg<65, std::uint64_t>), (bitset_test_cfg<130, std::uint8_t>) + + constexpr std::array patterns{ 0ULL, 1ULL, @@ -645,3 +650,61 @@ TEST_CASE("usable in constant expressions", "[bitset]") } #undef IRIS_TEST_BITSET_CFGS + +// ----------------------------------------------------- + +TEST_CASE("positions(): range properties", "[bitset]") +{ + using V = decltype(std::declval const&>().positions()); + STATIC_REQUIRE(std::ranges::view); + STATIC_REQUIRE(std::ranges::bidirectional_range); + STATIC_REQUIRE(std::ranges::common_range); + STATIC_REQUIRE(std::ranges::sized_range); + STATIC_REQUIRE(!std::ranges::random_access_range); + + using B = iris::bitset<100, std::uint8_t>; + constexpr auto b = B::from_positions({0, 7, 8, 63, 64, 99}); + STATIC_REQUIRE(std::ranges::equal(b.positions(), std::array{0, 7, 8, 63, 64, 99})); + STATIC_REQUIRE(std::ranges::equal(b.positions() | std::views::reverse, std::array{99, 64, 63, 8, 7, 0})); + STATIC_REQUIRE(b.positions().size() == 6); + STATIC_REQUIRE(B{}.positions().empty()); +} + +TEMPLATE_TEST_CASE("positions(): matches a naive scan in both directions", "[bitset]", IRIS_TEST_BITSET_POSITIONS_CFGS) +{ + constexpr std::size_t N = TestType::n; + using B = iris::bitset; + + std::vector inputs{B{}, B{}.set()}; + for (std::size_t offset = 0; offset < 3; ++offset) { + B b; + if constexpr (N > 0) { + for (std::size_t pos = offset; pos < N; pos += 3) { + b.set(pos); + } + } + inputs.push_back(b); + } + + for (auto const& b : inputs) { + std::vector expected; + if constexpr (N > 0) { + for (std::size_t pos = 0; pos < N; ++pos) { + if (b[pos]) expected.push_back(pos); + } + } + + auto const pv = b.positions(); + CHECK(std::ranges::equal(pv, expected)); + CHECK(std::ranges::equal(pv | std::views::reverse, expected | std::views::reverse)); + CHECK(pv.size() == expected.size()); + CHECK(B::from_positions(pv) == b); + + for (auto it = pv.begin(); it != pv.end(); ++it) { + auto j = std::next(it); + REQUIRE(std::prev(j) == it); + } + } +} + +#undef IRIS_TEST_BITSET_POSITIONS_CFGS