9
\$\begingroup\$

When I was looking to use std::bitset instead of GCC's __uint128 in my RDS error-corrector, I found that it was hard to use, because there's no way to widen or truncate a bitset to one of another size. Nor is there any ability to do bitwise arithmetic (&, |, ^) or comparison (==, !=) with a bitset of different width.

I've identified the functions that I believe ought to be part of std::bitset and implemented them by wrapping the standard class. To convert between different widths, I've had to go via std::string, but (as written in comment), an update to std::bitset would have a more direct (and exception-free) path.

I could have composed rather than inherited std::bitset - that's not really of concern here. Please ignore the mechanics that deal with my::bitset being different to std::bitset and imagine that they are a single, larger class. I'm most interested in whether the function signatures are correct and complete.

Note that (like the integers), widening can be implicit, but narrowing operations need to be explicit. Most of the apparent duplication is to handle this distinction.

#include <bitset>
#include <climits>
#include <type_traits>
#include <utility>
namespace my {
 template<std::size_t N>
 struct bitset : public std::bitset<N>
 {
 // forwarding of std::bitset constructors
 bitset()
 : std::bitset<N>{}
 {}
 template<typename T>
 requires std::is_unsigned_v<T>
 bitset(T n)
 : std::bitset<N>{n}
 {}
 template< class CharT, class Traits, class Alloc >
 explicit bitset(const std::basic_string<CharT,Traits,Alloc>& str,
 typename std::basic_string<CharT,Traits,Alloc>::size_type pos = 0,
 typename std::basic_string<CharT,Traits,Alloc>::size_type n =
 std::basic_string<CharT,Traits,Alloc>::npos)
 : std::bitset<N>{str, pos, n}
 {
 }
 // Widening and narrowing constructors, to be added to std::bitset
 // widening conversion
 template<std::size_t P>
 requires (P <= N)
 bitset(const std::bitset<P>& n) noexcept
 : std::bitset<N>{}
 {
 std::size_t i = n.size();
 while (i-- > 0) {
 this->set(i, n.test(i));
 }
 }
 // narrowing conversion
 template<std::size_t P>
 requires (P > N)
 explicit bitset(const std::bitset<P>& n) noexcept
 : std::bitset<N>{}
 {
 std::size_t i = this->size();
 while (i-- > 0) {
 this->set(i, n.test(i));
 }
 }
 };
 // Deduction guide for my::bitset
 template<std::size_t N>
 bitset(std::bitset<N>) -> bitset<N>;
 // Free functions to be added to namespace std
 // Deduction guide
 template<typename T>
 requires std::is_unsigned_v<T>
 bitset(T) -> bitset<sizeof (T) * CHAR_BIT>;
 // comparisons - widen as necessary
 template<std::size_t N, std::size_t Q>
 constexpr auto operator==(const bitset<N>& a, const bitset<Q>& b)
 requires (N != Q)
 {
 if constexpr (N > Q)
 return a == bitset<N>{b};
 else
 return bitset<Q>{a} == b;
 }
 template<std::size_t N, std::size_t Q>
 constexpr auto operator!=(const bitset<N>& a, const bitset<Q>& b)
 {
 return ! (a == b);
 }
 // bitwise-and produces the narrower type
 // (a specific exception to "doing as the integers do")
 template<std::size_t N, std::size_t Q>
 constexpr auto operator&(const bitset<N>& a, const bitset<Q>& b)
 requires (N > Q)
 {
 return bitset<Q>(a) & b;
 }
 template<std::size_t N, std::size_t Q>
 constexpr auto operator&(const bitset<N>& a, const bitset<Q>& b)
 requires (N < Q)
 {
 return a & bitset<N>(b);
 }
 // bitwise-and assignment accepts a wider type
 template<std::size_t N, std::size_t Q>
 constexpr auto operator&=(bitset<N>& a, const bitset<Q>& b)
 requires (N != Q)
 {
 return a &= bitset<N>(b);
 }
 // bitwise-or produces the wider type
 template<std::size_t N, std::size_t Q>
 constexpr auto operator|(const bitset<N>& a, const bitset<Q>& b)
 requires (N < Q)
 {
 return bitset<Q>{a} | b;
 }
 template<std::size_t N, std::size_t Q>
 constexpr auto operator|(const bitset<N>& a, const bitset<Q>& b)
 requires (N > Q)
 {
 return a | bitset<N>{b};
 }
 template<std::size_t N, std::size_t Q>
 constexpr auto& operator|=(bitset<N>& a, const bitset<Q>& b)
 requires (N >= Q)
 {
 return a = a | bitset<N>{b};
 }
 // bitwise-xor produces the wider type
 template<std::size_t N, std::size_t Q>
 constexpr auto operator^(const bitset<N>& a, const bitset<Q>& b)
 requires (N < Q)
 {
 return bitset<Q>{a} ^ b;
 }
 template<std::size_t N, std::size_t Q>
 constexpr auto operator^(const bitset<N>& a, const bitset<Q>& b)
 requires (N > Q)
 {
 return a ^ bitset<N>{b};
 }
 template<std::size_t N, std::size_t Q>
 constexpr auto& operator^=(bitset<N>& a, const bitset<Q>& b)
 requires (N >= Q)
 {
 return a = a ^ bitset<N>{b};
 }
 template<std::size_t N, typename T>
 requires std::is_unsigned_v<T>
 constexpr auto operator^(const bitset<N>& a, const T& b)
 {
 return a ^ bitset<N>{b};
 }
 template<std::size_t N, typename T>
 requires std::is_unsigned_v<T>
 constexpr auto operator^(const T& a, const bitset<N>& b)
 {
 return bitset<N>{a} ^ b;
 }
}

I have a simple compilation test for the above code. None of the mixed-width operations marked // new will compile with std::bitset; all the uncommented statements are fine with my::bitset. The commented-out statements are intended to be errors (because they have a narrowing conversion without a cast).

#define NEW_BITSET
int main()
{
#ifdef NEW_BITSET
 using my::bitset;
#else
 using std::bitset;
#endif
 // Lower-case variables are narrow; upper-case are wide
 bitset<32> a{0xFFFF0000u};
 bitset<64> B{a ^ 0xFFFF00u};
 //bitset<32> b = B; // error
 bitset<32> b{B}; // explicit conversion
 //auto b = bitset<32>{B}; // error
 bitset c = a & B; // new; c is bitset<32>
 bitset D = a | B; // new; D is bitset<64>
 bitset E = a ^ B; // new; E is bitset<64>
 c &= b; // no change
 c &= B; // new (unlike |= and ^=)
 B &= c; // new
 c |= b; // no change
 //c |= B; // error
 D |= b; // new
 c ^= b; // no change
 //c ^= B; // error
 D ^= b; // new
 D &= E; // no change
}

I probably ought to use the Detection Idiom to assert that something shouldn't be valid, but I resorted to uncommenting the // error lines one at a time to prove them wrong.


I'm particularly looking for any errors or omissions in the interface of my mixed-width operations, but will happily take any criticism at all.

asked Jun 13, 2019 at 17:43
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

One aspect that could surprise users is that operator & always narrows types to match. That's a departure from the usual guidance of "do what the integers do".

On one hand, it makes sense not to waste resources on bits that will always be zero. One of my own use cases is to extract the some or all of the lower 26 bits from a 130-bit value, so it seems reasonable to expect a 26-bit result.

On the other hand, it could catch users out when shifting the result. These two functions will give different results:

auto fun_i(std::uint32_t a, std::uint16_t b) {
 return (a & b) << 16;
}
auto fun_b(my::bitset<32> a, my::bitset<16> b) {
 return (a & b) << 16;
}

On balance, I think that having to explicitly widen before shifting is a fair price to pay for saving resources when masking large numbers, but that would need to be well documented. And I value dissenting opinions (in comments, please).

answered Jun 14, 2019 at 7:31
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.