This is a follow-up to Array-like container for uints shorter than 8 bits
In short: The PackedBitfieldArray
should be a kind of container for packed (unsigned) ints shorter than 8 bits. It provides proxy objects for manipulation and iterators.
What I have changed:
PackedBitfieldArray::end()
now returns a correct past-the-end iterator (that was broken in the previous version). There alsoconst
versions ofbegin()
andend()
.PackedBitfieldArray::operator[]()
now has aconst
companion.- The proxy objects
proxy
andconst_proxy
are now derived from aproxyT
template that implements theconst
version. The non-const version adds an assignment operator. - The iterators are now
random_access_iterator
(before:forward_iterator
, I've added the necessary decrement and offset (is that their name?) operators. - The iterators are now derived from a common template to minimize duplicate code (there is none, I think).
- The iterators now store an index where they stored an offset before. This might speed up everything related to random access, but slow down
++
,--
and repeated access to the same location.
Here's the code, questions and things I'd like to know better are below that.
Header:
#ifndef SFDD_PACKEDBITFIELDARRAY_H
#define SFDD_PACKEDBITFIELDARRAY_H
#include <array>
#include <cstddef>
#include <cstdint>
#include <iterator>
template<size_t Bits, size_t Elements, typename V = uint8_t>
class PackedBitfieldArray
{
public:
typedef V value_type;
static constexpr size_t value_bitwidth = 8*sizeof(value_type);
static_assert(Bits <= value_bitwidth, "Bit size must not be greater than the underlying storage type's bit size");
static_assert(value_bitwidth % Bits == 0, "Cannot pack this : (value_bitwidth % Bits per element) != 0");
static constexpr value_type arg_mask = (1<< Bits)-1;
// number of underlying storage elements
static constexpr size_t value_size = (Bits*Elements+(value_bitwidth-1))/value_bitwidth;
private:
template<bool isConst>
class proxyT
{
public:
typedef typename std::conditional<isConst,
const PackedBitfieldArray::value_type,
PackedBitfieldArray::value_type>::type value_type;
operator PackedBitfieldArray::value_type() const
{
return (data_ & (arg_mask << offset_)) >> offset_;
}
protected:
proxyT(value_type& data, size_t offset) : data_(data), offset_(offset) {}
value_type& data_;
size_t offset_;
};
public:
class proxy : public proxyT<false>
{
public:
proxy(value_type& data, size_t offset) : proxyT<false>(data, offset) {}
proxy& operator=(value_type value)
{
value_type orVal = ((value & arg_mask) << this->offset_);
this->data_ = (this->data_ & ~(arg_mask << this->offset_)) | orVal;
return *this;
}
};
class const_proxy : public proxyT<true> {};
value_type* data() {return data_.data();}
const value_type* data() const {return data_.data();}
void clear() {data_.fill(0);}
proxy operator[](size_t i)
{
size_t i_ = i*Bits/value_bitwidth;
uint8_t offset = i * Bits % value_bitwidth;
return proxy(data()[i_], offset);
}
const_proxy operator[](size_t i) const
{
size_t i_ = i*Bits/value_bitwidth;
uint8_t offset = i * Bits % value_bitwidth;
return const_proxy(data()[i_], offset);
}
template<bool isConst>
class iteratorT
{
public:
typedef std::random_access_iterator_tag iterator_category;
typedef typename std::conditional<isConst, const_proxy, proxy>::type value_type;
typedef typename std::conditional<isConst, const PackedBitfieldArray::value_type, PackedBitfieldArray::value_type>::type storage_type;
typedef value_type& reference;
typedef value_type* pointer;
typedef ptrdiff_t difference_type;
/** Constructor **/
iteratorT(storage_type* data, size_t i) : data_(data), i_(i) { }
/**
Input iterator, Forward Iterator
**/
iteratorT& operator++()// prefix increment
{
i_++;
return *this;
}
iteratorT operator++(int)// postfix increment
{
iteratorT previous(*this);
operator++();
return previous;
}
bool operator==(const iteratorT& rhs) const
{
return ((data_ == rhs.data_) && (i_ == rhs.i_));
}
bool operator!=(const iteratorT& rhs) const {return !(operator==(rhs));}
proxy operator*() const
{
size_t dataIndex = i_*Bits/value_bitwidth;
uint8_t offset = i_ * Bits % value_bitwidth;
return proxy(data_[dataIndex], offset);
}
/**
Bidirectional iterator
**/
iteratorT& operator--()
{
i_--;
}
iteratorT operator--(int)
{
iteratorT previous(*this);
operator--();
return previous;
}
/**
Random Access Iterator
**/
iteratorT& operator+=(size_t n)
{
i_ += n;
return *this;
}
iteratorT operator+(size_t n) const
{
iteratorT result(*this);
result+= n;
return result;
}
iteratorT& operator-=(size_t n)
{
i_ -= n;
return *this;
}
iteratorT operator-(size_t n) const
{
iteratorT result(*this);
result-= n;
return result;
}
/**
Difference (do I need further comparisons?)
**/
typename iteratorT::difference_type operator-(const iteratorT& rhs) const
{
return i_ - rhs.i_;
}
private:
storage_type* data_;
size_t i_;
};
typedef iteratorT<false> iterator;
typedef iteratorT<true> const_iterator;
iterator begin()
{
return iterator(data_.data(), 0);
}
iterator end()
{
return iterator(data_.data(), Elements);
}
const_iterator begin() const
{
return const_iterator(data(), 0);
}
const_iterator end() const
{
return const_iterator(data_.data(), Elements);
}
private:
std::array<value_type, value_size> data_;
};
#endif // SFDD_PACKEDBITFIELDARRAY_H
Usage Example
#include <algorithm>
#include <bitset>
#include <iostream>
#include "packedBitfieldArray.h"
int main()
{
static const size_t bits = 2;
static const size_t n = 9;
typedef PackedBitfieldArray<bits, n, uint8_t> PackedBitfield;
PackedBitfield a;
using namespace std;
cout << "bits = " << bits << ", n = " << n << ", [a] = " << sizeof(a) << "\n";
cout << "a.arg_mask = 0b" << std::bitset<8*sizeof(PackedBitfield::value_type)>(a.arg_mask) << "\n\n";
a.clear();
/* Fill with indices */
for (size_t i = 0; i < n; i++)
{
a[i] = i;
}
cout << "\nfor loop, C-like element access:\n";
for (size_t i = 0; i < n; i++)
{
cout << "a[" << i << "] = " << (int)a[i] << "\n";
}
cout << "\nfor loop with iterators:\n";
for (auto it = a.begin(); it != a.end(); ++it)
{
cout << "a[...] = " << *it;//(int)(uint8_t)*it;
cout << " (0b" << std::bitset<8*sizeof(PackedBitfield::value_type)>(*it) << ")\n";
}
cout << "\nfor_each with a lambda:\n";
std::for_each(a.begin(), a.end(), [](const PackedBitfield::proxy pr) {cout << "a[...] = " << (int)pr << " (0b" << std::bitset<8*sizeof(PackedBitfield::value_type)>(pr) << ")\n";});
cout << "\nunderlying data dump:\n";
for (size_t i = 0; i < a.value_size; i++)
{
cout << "a[" << i << "_] = " << (int)a.data()[i] << " (0b" << std::bitset<8*sizeof(PackedBitfield::value_type)>(a.data()[i]) << ")\n";
}
cout << "\nfor loop, random access offset:\n";
for (size_t i = 0; i < n; i++)
{
auto it = a.begin() + i;
cout << "a[" << i << "] = " << (int)*it << "\n";
}
size_t start = 2;
cout << "\nfor loop, random access offset from " << start << ":\n";
auto iStart = a.begin() + start;
for (size_t i = start; i < n; i++)
{
auto it = iStart + (i - start);
cout << "a[" << i << "] = " << (int)*it << "\n";
}
const PackedBitfield b = a;
auto icStart = b.begin();
return 0;
}
Questions and Problems
Source
- Are the comments ok (amount, type, and quality)?
- Is the formatting ok?
PackedBitfieldArray
- I have defined a
value_type
which is the underlying storage type, seePackedBitfieldArray::data_
, which is astd::array
). Should I add a second typedef for read/write access using proxies? This would add some flexibility, but make it harder to understand/use. The names would then bestorage_type
andvalue_type
.
proxyT
- this class is private to
PackedBitfieldArray
and has a protected constructor. It can only be constructed byproxy
andconst_proxy
, which derive from it. - I don't know how well the
operator value_type()
behaves when other int-like types must be assigned the proxy's value. When I run into problems with that, should I add a template conversion operator that handles integers or would that create even more problems? - I will most probably have statements like
bool b = somePackedBitfieldArray[i]
. Implicit conversion to bool is said (! I have no real experience with that) to be dangerous. What should I prepare for regarding implicit conversions to bool?
const_proxy
- derived from
proxyT
, but doesn't refine anything. Atypedef proxyT<true> const_proxy
wouldn't work here, because it couldn't be constructed: the base constructor is protected. Is that particularly bad?
proxy
- Similar to the
proxyT
conversion operator, will I run into problems with the assignment operator here?
iteratorT
- The internal types seem to work, but I can't see any sense for
reference
andpointer
types, because the interface uses the proxy objects. However, containers should allow references and pointers to the objects they store, right? - is
proxy
/const_proxy
the correctvalue_type
in the first place? - Do I need more comparison operators, such as
<
,>
,<=
,>=
?
Testing
- How do I test this (the container, the iterators) without too much trial and error?