【问题标题】:Is there a way I can use a 2-bit size type instead of an int, by just plugging in the new type name instead of int?有没有办法通过插入新类型名称而不是 int 来使用 2 位大小的类型而不是 int?
【发布时间】:2018-11-18 19:26:17
【问题描述】:

我有一个应用程序需要尽可能多地节省内存。我需要存储大量可以取三个可能值的数据。所以,我一直在尝试使用 2 位大小的类型。

一种可能性是使用位域。我可以的

struct myType {
    uint8_t twoBits : 2;
}

这是来自this thread的建议。

但是,在此之前我使用过int 变量的任何地方,我都需要通过附加.twoBits 来更改它们的用法。我检查了是否可以在struct 之外创建一个位字段,例如

uint8_t twoBits : 2;

this thread 说这是不可能的。但是,该线程是特定于 C 的,所以我不确定它是否适用于 C++。

有没有一种干净的方法可以定义 2 位类型,这样只需将 int 替换为我的类型,我就可以正确运行程序?还是使用位域是唯一可能的方法?

【问题讨论】:

  • 存在定位宏。假设您使用的是 gcc。我相信其他人有自己的方法。 gcc.gnu.org/onlinedocs/gcc-3.0.4/gcc_21.html 特别是 INT_TYPE_SIZE 存在。从来没有尝试过自己使用它,所以ymmv
  • 你有两个位,但结构仍然是一个字节。 myType 的数组将是最小可能大小的 4 倍。您可能会发现自己不得不打包和拆包。
  • @user4581301 谢谢。有没有一种干净的方法来定义 myType 以便在不更改任何外部代码的情况下完成打包和解包?
  • 是的。您将打包的位数组包装成一个看起来像整数数组的结构。您可以实现operator[],它为您找到正确的字节并返回对代理类实例的引用,该代理类隐式转换为int 并具有知道如何转换intoperator=进入位数组中的正确位置。不幸的是,我没有时间写一个并证明它现在可以正常工作。听起来会很有趣。

标签: c++ types int bit-fields


【解决方案1】:

简单地说,构建在 bitset 之上,类似于:

#include<bitset>
#include<iostream>
using namespace std;
template<int N> 
class mydoublebitset 
{
public: 
    uint_least8_t operator[](size_t index)
    {
        return 2 * b[index * 2 + 1] + b[index * 2 ];
    }

    void set(size_t index, uint_least8_t store)
    {
        switch (store)
        {
        case 3:
            b[index * 2] = 1;
            b[index * 2 + 1] = 1;
            break;
        case 2:
            b[index * 2] = 0;
            b[index * 2 + 1] = 1;
            break;
        case 1:
            b[index * 2] = 0;
            b[index * 2 + 1] = 1;
            break;
        case 0:
            b[index * 2] = 0;
            b[index * 2 + 1] = 0;
            break;
        default:
            throw exception();
        }
    }
private:
    bitset<N * 2> b;
};

int main()
{

    mydoublebitset<12> mydata; 

    mydata.set(0, 0);
    mydata.set(1, 2);
    mydata.set(2, 2);

    cout << (unsigned int)mydata[0] << (unsigned int)mydata[1] << (unsigned int)mydata[2] << endl;
    system("pause");
    return 0;
}

基本上使用两倍大小的位集并相应地对其进行索引。根据您的要求,它更简单且内存效率更高。

【讨论】:

    【解决方案2】:

    具有代理类的位数组的一个非常小的示例(大部分情况下)看起来就像您正在处理一个非常小的整数数组。

    #include <cstdint>
    #include <iostream>
    #include <vector>
    
    class proxy
    {
        uint8_t & byte;
        unsigned int shift;
    public:
        proxy(uint8_t & byte,
              unsigned int shift):
                  byte(byte),
                  shift(shift)
        {
    
        }
        proxy(const proxy & src):
            byte(src.byte),
            shift(src.shift)
        {
    
        }
        proxy & operator=(const proxy &) = delete;
        proxy & operator=(unsigned int val)
        {
            if (val <=3)
            {
                uint8_t wipe = 3 << shift;
                byte &= ~wipe;
                byte |= val << shift;
            }
            // might want to throw std::out_of_range here
            return *this;
        }
        operator int() const
        {
            return (byte >> shift) &0x03;
        }
    };
    

    Proxy 持有对字节的引用,并且知道如何提取两个特定位,并且对于任何使用它的人来说都像 int

    如果我们用一个类包装一个字节数组,该类返回这个代理对象包装在适当的字节周围,我们现在有一个看起来很像一个非常小的ints 数组的东西。

    class bitarray
    {
        size_t size;
        std::vector<uint8_t> data;
    public:
        bitarray(size_t size):
            size(size),
            data((size + 3) / 4)
        {
    
        }
        proxy operator[](size_t index)
        {
            return proxy(data[index/4], (index % 4) * 2);
        }
    };
    

    如果您想扩展它并走得更远,Writing your own STL Container 应该可以帮助您制作一个全副武装且可操作的位封装阵列。

    这里有滥用的余地。调用者可以保持proxy 并采取任何这种允许的邪恶方式。

    使用这个原始示例:

    int main()
    {
        bitarray arr(10);
    
        arr[0] = 1;
        arr[1] = 2;
        arr[2] = 3;
        arr[3] = 1;
        arr[4] = 2;
        arr[5] = 3;
        arr[6] = 1;
        arr[7] = 2;
        arr[8] = 3;
        arr[9] = 1;
    
        std::cout << arr[0] << std::endl;
        std::cout << arr[1] << std::endl;
        std::cout << arr[2] << std::endl;
        std::cout << arr[3] << std::endl;
        std::cout << arr[4] << std::endl;
        std::cout << arr[5] << std::endl;
        std::cout << arr[6] << std::endl;
        std::cout << arr[7] << std::endl;
        std::cout << arr[8] << std::endl;
        std::cout << arr[9] << std::endl;
    
    }
    

    【讨论】:

    • 谢谢。我是否正确,proxy 不会产生大的内存开销,因为只有在访问和销毁之后才创建类的成员?
    【解决方案3】:

    我不相信你会用你现有的结构保存任何东西,因为周围的结构仍然会四舍五入到整数字节。

    您可以编写以下代码将 4 个 2 位计数器压缩为 1 个字节,但正如您所说,您必须将它们命名为 myInst.f0:

    struct MyStruct
    {
      ubyte_t  f0:2,
               f1:2,
               f2:2,
               f3:2;
    } myInst;
    

    在 c 和 c++98 中,您可以声明此匿名,但不推荐使用此用法。您现在可以通过名称直接访问这 4 个值:

    struct 
    { // deprecated!
      ubyte_t  f0:2,
               f1:2,
               f2:2,
               f3:2;
    };
    

    您可以声明某种模板,该模板使用运算符 int 和运算符 =(int) 包装单个实例,然后定义一个联合以将 4 个实例放在同一位置,但同样不推荐使用匿名联合。但是,您随后可以声明对 4 个值的引用,但您要为引用付费,这些引用比您尝试保存的字节大!

    template <class Size,int offset,int bits>
    struct Bitz
    {
        Size ignore : offset,
             value : bits;
        operator Size()const { return value; }
        Size operator = (Size val) { return (value = val); }
    };
    template <class Size,int bits>
    struct Bitz0
    {   // I know this can be done better
        Size value : bits;
        operator Size()const { return value; }
        Size operator = (Size val) { return (value = val); }
    };
    
    static union
    {   // Still deprecated!
        Bitz0<char, 2> F0;
        Bitz<char, 2, 2> F1;
        Bitz<char, 4, 2> F2;
        Bitz<char, 6, 2> F3;
    };
    
    union
    {
        Bitz0<char, 2> F0;
        Bitz<char, 2, 2> F1;
        Bitz<char, 4, 2> F2;
        Bitz<char, 6, 2> F3;
    } bitz;
    Bitz0<char, 2>& F0 = bitz.F0; /// etc...
    

    或者,您可以简单地声明宏以用简单的名称替换带点的名称(1970 年代如何):

    #define myF0 myInst.f0

    请注意,您不能通过引用或指针传递位域,因为它们没有字节地址,只能通过值和赋值。

    【讨论】:

      【解决方案4】:

      有没有一种干净的方法可以定义一个 2 位类型,所以只需 用my类型替换int,我可以正确运行程序吗?或者是 使用位域是唯一可能的方法吗?

      您可以尝试通过提供隐式转换运算符和构造函数来使结构尽可能透明:

      #include <cstdint>
      #include <iostream>
      
      template <std::size_t N, typename T = unsigned>
      struct bit_field {
          T rep : N;
          operator T() { return rep; }
          bit_field(T i) : rep{ i } { }
          bit_field() = default;
      };
      
      using myType = bit_field<2, std::uint8_t>;
      
      int main() {
          myType mt;
          mt = 3;
          std::cout << mt << "\n";
      }
      

      所以my_type 类型的对象有点像真正的 3 位无符号整数,尽管有超过 3 位。 当然,剩余位是未使用的,但由于在大多数系统上单个位不可寻址,这是最好的方法。

      【讨论】:

      • 谢谢。为了节省剩余位,我可以在一个结构中拥有 4 个大小为 2 位的位字段吗?如果是这样,如果我有一个 my_type 本身的向量,是否可以像普通数组/向量一样取消引用它们?
      • 这看起来很像std::bitset(可索引@GoodDeeds)
      • @vu1p3n0x 是的,但它的行为更像是一种基本类型,而不是一组位,例如在算术、I/O 等方面。
      【解决方案5】:

      CPU,因此内存、总线和编译器也只使用字节或字节组。如果不存储其他 6 位,就无法存储 2 位类型。

      你可以定义一个只使用一些位的结构。但我们知道它不会节省内存。

      您可以在一个结构中打包多个 x-bits 类型,正如您已经知道的那样。或者您可以进行位操作将它们打包/解包为整数类型。

      【讨论】:

      • 谢谢。有没有一种干净的方法来定义 myType 以便在不更改任何外部代码的情况下完成打包和解包?
      • @GoodDeeds 问题是当您检索一个变量(如myContainer.GetVar)时,您还必须告诉您想要哪些数据(可能有 4 个)。您可能有 4 种不同的类型 myType1myType2 等等,它们指的是职位。但是,如果您不关心其他事情,那将是一团糟。
      猜你喜欢
      • 1970-01-01
      • 2022-01-13
      • 1970-01-01
      • 2017-10-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多