如何在c++中正确地表示非完整字节数的数组?

zpqajqem  于 2023-06-25  发布在  其他
关注(0)|答案(2)|浏览(171)

我想用C++实现FAT12规范,其中FAT是一个12位数字的数组。
因为类型只能有完整的字节大小,所以我试着在结构体中使用位域来获得一对两个12位的数字,这将填充3个字节:

struct TwoEntries {
    uint16_t first : 12;
    uint16_t second : 12;
};

但是由于this question中解释的填充,这个结构体的大小为4个字节,并且使用填充时数组将无法正确地容纳数据。
所以我的问题是:
有没有一些方法可以正确地声明一个12位数字的数组?

5cnsuln7

5cnsuln71#

创建一个压缩字节数组很容易-你可以使用uint8_t的数组(或向量)。棘手的是将该数组中的12位视为12位int,因为没有C++类型表示“12位整数”。
但是,我们可以创建一个近似于12位整数的引用的代理类型:

class TwelveBitInt {
public:
   // Our 12-bit int starts at byte offset "byte", bit offset "bit"
   TwelveBitInt( uint8_t* byte, int bit ) : ptr{byte}, start_position{bit} {}

   operator uint16_t() const {
      // return bit manipulation to extract your 12-bit number
   }

   TwelveBitInt& operator=( uint16_t new_value ) {
      // use bit manipulation to assign new_value to the underlying array
   }

private:
   uint8_t* ptr;
   int start_position;
};

只要不太仔细看,这就给了你一个看起来像12位整数的类型。它可以隐式地转换为uint16_t,并且可以从uint16_t赋值,这对于大多数用途来说已经足够好了。至于它是否是可移植的,这取决于您在位操作中所做的假设,但这取决于您。
然后我会为你的数组写一个容器类。为了简单起见,我假设数组的大小在构造时已知。

class Fat12Array {
public:
   Fat12Array( std::size_t n_elems )
     : bytes( (n_elems * 3 + 1) / 2, 0 ) {}

   TwelveBitInt operator[]( std::size_t idx ) {
      // Interpret underlying bytes as an array of 3-byte/2-elem

      // Get address of the 3-byte structure
      auto byte_ptr = bytes.data() + 3*(idx/2);

      if( idx % 2 ) {
          return TwelveBitInt{ byte_ptr + 1, 4 };
      } else {
          return TwelveBitInt{ byte_ptr, 0 };
      }
   }

private:
   std::vector<uint8_t> bytes;
};

取决于你想做得有多花哨,你可以处理const TwelveBitInts,向容器添加更多的方法,也许是迭代器,等等,但这是基本的想法。

tp5buhyn

tp5buhyn2#

  • 技术上 * 有一种方法,但它不是便携式的:
#include <cstdint>

struct [[gnu::packed]] TwoEntries {
    std::uint16_t first : 12;
    std::uint16_t second : 12;
};

static_assert(sizeof(TwoEntries) == 3); // assertion passes

位字段成员的大小(以字节为单位),它们之间的填充以及其他属性完全是实现定义的,因此当处理像文件系统这样的东西时,它们会成为一个可怕的工具,因为在文件系统中,您必须有一个对所有编译器都相同的布局。
相反,考虑创建一个具有完全控制布局的类:

struct TwoEntries {
    std::uint8_t data[3];

    std::uint16_t get_first() const {
        return data[0] | ((data[1] & 0xf) << 8);
    }

    std::uint16_t get_second() const {
        return ((data[1] >> 4) & 0x0f) | (data[2] << 4);
    }

    void set_first(std::uint16_t x) {
        data[0] = x & 0xff;
        data[1] = (data[1] & 0xf0) | ((x >> 8) & 0x0f);
    }

    void set_second(std::uint16_t x) {
        data[1] = ((x & 0x0f) << 4) | (data[1] & 0xf);
        data[2] = (x >> 4) & 0xff;
    }
};

正如你所看到的,这个方法比使用位字段要多花很多功夫,但是我们可以完全控制内存布局,而且我们的解决方案可以跨不同的编译器移植。
如果你经常遇到这种模式,创建一个类模板可能是有意义的,比如:

template <std::size_t BitWidth, std::size_t BitOffset>
struct uint_bitref {
    void* to;
    uint_bitref(void* to) : to{to} {}
    /* ... */
};

// and then implement TwoEntries by returning this reference
// which we can use to read and write an integer at a certain bit offset

struct TwoEntries {
    using first_t = uint_bitref<12, 0>;
    using second_t = uint_bitref<12, 4>;

    std::uint8_t data[3];
    
    first_t first() {
        return data;
    }

    second_t get_second() {
        return data + 1;
    }
};

相关问题