Bit manipulator (reader / writer) Please take a review of my simple bit manipulator:
#ifndef BIT_STREAM_H
#define BIT_STREAM_H

#include &lt;cstdint&gt;

class BitStream {
public:
    explicit BitStream(uint8_t* buffer, size_t buffer_size) : m_Buffer(buffer), m_BufferSize(buffer_size) {

    }

    virtual ~BitStream() = default;

public:
    inline uint64_t Mask(uint64_t offset, size_t size) const {
        return ((1 &lt;&lt; size) - 1) &lt;&lt; offset;
    }

public:
    inline size_t GetBufferSize() const {
        return m_BufferSize;
    }

    inline size_t GetOffset() const {
        return m_Offset;
    }

    inline bool GetErrorState() const {
        return m_ErrorState;
    }

protected:
    uint8_t* m_Buffer = nullptr;
    size_t m_BufferSize = 0;
    size_t m_Offset = 0;
    bool m_ErrorState = false;
};

class BitReader : public BitStream {
public:
    explicit BitReader(uint8_t* buffer, size_t buffer_size) : BitStream(buffer, buffer_size) {

    }

    template &lt;size_t buffer_size&gt;
    explicit BitReader(uint8_t(&amp;buffer)[buffer_size]) : BitStream(buffer, buffer_size) {

    }

public:
    template&lt;const size_t bit_size, typename value_t = uint64_t&gt;
    inline value_t Read(value_t df = 0) {
        const size_t bpb = 8;
        const size_t byte_offset = m_Offset / bpb;
        const size_t relative_offset = m_Offset - (byte_offset * bpb);

        if (m_ErrorState) {
            return df;
        }

        if (((m_Offset + bit_size) / bpb) &gt;= m_BufferSize) {
            m_ErrorState = true;
            return df;
        }

        m_Offset += bit_size;

        return (*(value_t*)&amp;m_Buffer[byte_offset] &amp; Mask(relative_offset, bit_size)) &gt;&gt; relative_offset;
    }
};

class BitWriter : public BitStream {
public:
    explicit BitWriter(uint8_t* buffer, size_t buffer_size) : BitStream(buffer, buffer_size) {

    }

    template &lt;size_t buffer_size&gt;
    explicit BitWriter(uint8_t(&amp;buffer)[buffer_size]) : BitStream(buffer, buffer_size) {

    }

public:
    template&lt;const size_t bit_size, typename value_t = uint64_t&gt;
    inline void Write(value_t value) {
        const size_t bpb = 8;
        const size_t byte_offset = m_Offset / bpb;
        const size_t relative_offset = m_Offset - (byte_offset * bpb);

        if (m_ErrorState) {
            return;
        }

        if (((m_Offset + bit_size) / bpb) &gt;= m_BufferSize) {
            m_ErrorState = true;
            return;
        }

        m_Offset += bit_size;

        *(value_t*)&amp;m_Buffer[byte_offset] = (*(value_t*)&amp;m_Buffer[byte_offset] &amp; ~Mask(relative_offset, bit_size)) | (value &lt;&lt; relative_offset);
    }
};

#endif // BIT_STREAM_H


Example of usage:
#include &quot;BitStream.h&quot;

#include &lt;iostream&gt;
#include &lt;cassert&gt;

int main() {
    uint8_t buffer[2];
    memset(buffer, 0, sizeof(buffer));

    BitWriter writer(buffer);

    writer.Write&lt;4&gt;(15);
    writer.Write&lt;2&gt;(0);
    writer.Write&lt;6&gt;(63);

    assert(!writer.GetErrorState());

    BitReader reader(buffer);

    std::cout &lt;&lt; reader.Read&lt;4&gt;() &lt;&lt; std::endl;
    std::cout &lt;&lt; reader.Read&lt;2&gt;() &lt;&lt; std::endl;
    std::cout &lt;&lt; reader.Read&lt;6&gt;() &lt;&lt; std::endl;

    assert(!reader.GetErrorState());

    std::cin.ignore();
}

Later in plan add exceptions, to string converter method.
Inline isn't
This pops up on CodeReview regularly. inline is effectively ignored by most modern compilers, and even if it weren't, it isn't a good idea to force the matter -- programmers do not know what's best for their program when compared to the compiler when full optimization is enabled. It's safe to omit this.
Redundant initial values
uint8_t* m_Buffer = nullptr;
size_t m_BufferSize = 0;

These are initialized by your constructor, so there's no point in pre-initializing them here.
Type punning
This:
*(value_t*)&amp;m_Buffer[byte_offset]

is a little gross. There are better ways to do inline pointer casts, particularly in C++. Here, since you're punning from uint8_t* to a pointer of arbitrary type, you'll probably want reinterpret_cast.
I guess bit_size should be 64 if you pass uint64_t for value_t, but I don't see why it's passed separately. You can move bpb up in scope; then bit_size would equal bpb * sizeof(value_t). Perhaps you want to support bit_size smaller than the value type, which is fine; but you should at least assert that bit_size is less than or equal to the number of bits in value_t.
There are subtler risks in the way that you're packing bits - from what I can tell, it assumes little-endian, which will not be universally true.