CUJ:标准库:bitset和bit vector (转)
Matt Austern
http://www.cuj.com/experts/1905/austern.htm?topic=experts
在 C++里,你能如愿地玩弄位元,而且甚至不用到宏。
------------------------------------------------------------------------------
编过程的人都熟悉布尔选项标志:将一组选项处理成一个整体,将它们打包进一个word,为每个选项使用一个位。比如,要设置unix文件的许可权限,你可能类似于这样写:
chmod("my_file",
S_IWUSR | S_IRUSR |
S_IRGRP | S_IROTH);
每个常量对应一个位;通过用“位或”操作组合它们,你能够一次就指定很多选项。
将多个选项位打包进一个word的行为非常常见。这个技巧被用于很多地方,在Unix和win32的api中,在C++标准运行库的ios_base格式化标志中,并且它的一些形式容易在大型的程序中出现。位元的集合是很重要的。
不难理解为什么这个技巧很常见:另外一种实现方法是,用数组或结构,每个选项对应一个不同的字段,这比较笨拙并且浪费内存。然而,有时候这个技巧会造成麻烦。首先,某些运算可能是笨拙的:设置一个被命名的位还比较直接(flags |= S_IRGRP),但清除一个位(flages &= ~S_IWGRP)多少有些丑陋。你能测试一个位是否被置位,通过将它掩模:if (falgs & S_IWUSR);但当心犯“显式”测试的错误:if ((flags & S_IWUSR) == true),或更糟的if (flags & S_IWUSR == ture)。对应于命名的位,对于编号的位,同样笨拙:需要使用类似于flags &= ~(1 << n)这样的表达式,通常还要加上强制类型转换。最后,这个技巧难以括大到有很多选项的情况:一旦标志的数目超过long的位数,就需要另谋它路了。
因为位元的集合很重要,所以C++标准运行库提供了对它们的显式支持--事实上,有几种支持。有时你将会仍然想使用低层次的位操作(并且,某些时候你不得不这么做,如果你正在和C语言API交互),但是在绝大部份情况下,C++运行库中的版本会更合适。它们有一些小问题,但绝大部份很容易绕过。
类std::bitset出现在C++标准第23章“关联容器”中。这不是它应该出现的正确位置,因为bitset与set和map这样的关联容器没有任何关系,它甚至没有满足STL容器的最基本的需求。把bitset当做一个整数会更好,它的每个的位都能单独访问--但它不受long的长度的限制。bitset的长度大小在编译期决定(位元的数目是模板参数),但是没有上限:bitset<32>是32位长,bitset<1000>是1000。
你用过的整型位操作对bistset继续有效,并且,为了方便,还新增了一些操作。举例来说,你能写b1 ^ b2来进行“位或”运算(至少在b1和 b2长度相同时)。操作单个位有两个不同的接口:你可以用b.set(n)设定第n个位,用b.reset(n)清除它,并且用if (b.test(n))测试它;或者,几乎等价地,你可以把bitset当做一个数组,用b[n] = true,b[n] = false,和if (b[n])来实现相同的操作。(称“几乎”,是因为有一个小小的差异:数组版本不进行越界检查,而set()/reset()/test()版本进行。如果传给set()/reset()/test()的参数太大,将得到out_of_range异常。)
如果你用的bitset大小适当,直觉上你能将它当作一个整数:有一个构造函数以从unsigned long创建一个bitset,还有一个成员函数to_ulong()以从bitset获得一个unsigned long。当然,你不能直接使用这个构造函数以初始化超过unsigned long范围的位;同样,你不能用to_ulong()提取超过unsigned long的位。(如果你试着做了, 并且超过unsigned long的任何一位被置位了,那么to_ulong()将会抛出一个异常)。然而,如果需要,你能通过使用移位和掩模来绕开这些限制:
const int N =
sizeof(unsigned long) * CHAR_BIT;
unsigned long high = 0x7B62;
unsigned long low = 0x1430;
std::bitset<2*N> b
= (std::bitset<2*N>(high) << N) |
std::bitset<2*N>(low);
...
const std::bitset<2*N>
mask((unsigned long)(-1));
low = (b & mask).to_ulong();
high = (b >> N).to_ulong();
第0个位被定义为最低有效位,所以,举例来说,如果你写:
std::bitset<4> b(0xA);
被置位的位是b[1]和b[3]。
很容易用bitset替代传统的选项标志:只要在头文件中申明一个bitset对象以替代整数常量。我们已经说到了使用bitset的两个好处:你得到比long所能表示的更多的标志,你能用更容易和更安全的方法来操作每个位。另外一个是,bitset给你一个转换机制,以在bitset和文字表述间双向转换。
首先,bitset提供了常用的I/O操作。这个程序,
#include
#include
int main() {
std::bitset<12> b(3432);
std::cout << "3432 in binary is "
<< b << std::endl;
}
给出了直观的结果:
3432 in binary is 110101101000.
输入操作以同样的方法工作:它读入一个“1”和“0”组成的字符串,将它们转换为一个bitset。
其次,你能将bitsets转换成字符串或从字串转换而来:有一个接受一个string参数的构造函数,和bitset<>::to_string()成员函数。唉,虽然这些转换很有用,但细节表明它非常不方便。接受string的构造函数和to_string()成员函数都是成员模板,因为运行库的std::basic_string类本身就是模板;平常的字符串类,std::string是basic_string
这些成员模板的通用性受C++的一些晦涩的规则的不幸影响。你必须写:
std::bitset<6>(std::string("110101"));
而不是
std::bitset<6>("110101");
仅将字符串文字“110101”直接传入的版本,将会给出编译期错误,因为编译器不知道该实例化出成员模板的什么版本。同样地,如果b 是bitset,你不能只是写:
std::string s = b.to_string();
你必须改用这种实在恐怖的形式:
std::string s
= b.template to_string std::char_traits std::allocator (是的,那个看起来可笑的template关键字真的是必须的。) 当然,在实践中,你不应该用这样的东西污染你的代码。除非真的需要和多种字符类型合作,你可以将恐怖的语法细节封装入辅助函数: template std::bitset from_string(const std::string& s) { return std::bitset } template std::string to_string (const std::bitset return b.template to_string std::char_traits std::allocator } bitset确实有一个重要限制:它有一个固定的长度。你可以有比long更长的bitset,但你必须事先指定它的大小。对选项标志集之类的东西,这很好,但用于其它目的就不怎么合适了。假如,你正在以复杂的顺序处理一个巨大的条款集,并且你需要掌握已经看过哪些。这要求一个布尔值的数组,有理由使用“压缩”的数组,每个元素用一个位,但bitset就不再是合理的选择了。你正在处理的条款的数目直到运行时才能知道,并且条款甚至可能增加或移除。 C++标准运行库中另外一个管理位集合的机制是vector 虽然vector std::vector std::transform(v1.begin(), v1.end(), v2.begin(), v3.begin(), std::logical_and 同样地,将vector std::copy(v.rbegin(), v.rend(), std::ostream_iterator (这个代码依赖于一个事实,默认情况,bool的输出使用“1”和“0”而不是“true”和“false”。同样也注意到,我们正在使用 rbegin()和rend()以按逆序拷贝vector 只要有可能,你应该总是优先使用bitset而不是vector 看起来还有一个情况应该用vector 然而,你不应该让这个缺乏阻止你!虽然bitset没有STL容器的接口,它仍然是一个非常好的(固定大小的)容器。如果使用bitset有意义,并且如果你还需要选择子,那么你可以定义一个简单的“下标选择子”适配器,以将选择子表述(比如*i)转换为数组表述(比如b[n])。 实现很显然:维护一个下标和指向容器的指针。其细节,大部分就是我们在实现random iterator时所用到的,见于Listing 1。我们也定义一些非成员的辅助函数,begin()和end(),它们接受一个bitset为参数。(我们在Listing 1中显示的iterator不像它可能的那样通用:如果我们愿意接受稍微有些笨重的接口,我们就能定义一个能与任何类似与数组的类型合作的类了。一个通用目的下标选择子适配器在处理pre-STL容器类时常常有用,有时,即使是处理STL容器比如vector时都很有用。) 使用bitset_iterator,bitset现在能与STL组件交互:比如,你能将一个bitset拷贝入vector std::bitset<10> b; ... std::vector b(begin(b), end(b)); 然而,如果你仔细看过Listing 1,你可能已经注意到bitset_iterator的一个问题:名字是一个谎言,因为bitset_iterator并不真的是一个iterator。如果i是一个iterator,那么*i应该返回i所指对象的引用。bitset_iterator没有这样做:const bitset_iterator返回bool,而不是const bool&,而可修改版本的bitset_iterator返回一个类型是bitset<>::reference的代理对象,而不是bool&。 因为位不是可独立寻址的,这是我们所能做的最好了;事实上,vector 布尔值的数组在大程序中很常见,并且C++标准运行库提供了几个方法来表示这样的数组。我没有列全所有的可能:比如,你能使用valarray 很多时候,无论如何,最容易的方法是使用std::bitset。如果你在编译期知道你的布尔数组应该多大,或至少能指定一个合理的上限,那么bitset更简单,更有效率。在bitset的接口上有些讨厌的问题,但藉由一些辅助函数,很容易绕过它们。 template struct IF; template struct IF typedef IfTrue val; }; template struct IF typedef IfFalse val; }; template class bitset_iterator { private: typedef std::bitset typedef typename IF qBitset; typedef std::random_access_iterator_tag iterator_category; typedef bool value_type; typedef std::ptrdiff_t difference_type; typedef typename IF pointer; typedef typename IF bool, typename bitset::reference>::val reference; qBitset* B; std::size_t n; public: bitset_iterator() : B(), n() { } bitset_iterator(qBitset& b, std::size_t sz) : B(&b), n(sz) { } bitset_iterator(const bitset_iterator : B(x.B), n(x.n) { } bitset_iterator& operator=(const bitset_iterator& x) { B = x.B; n = x.n; } public: reference operator*() const { return (*B)[n]; } reference operator[](std::ptrdiff_t x) const { return (*B)[n + x]; } bitset_iterator& operator++() { ++n; return *this; } bitset_iterator operator++(int) { ++n; return bitset_iterator(*B, n-1); } bitset_iterator& operator--() { --n; return *this; } bitset_iterator operator--(int) { --n; return bitset_iterator(*B, n+1); } bitset_iterator operator+(std::ptrdiff_t x) const { return bitset_iterator(*B, n + x); } bitset_iterator& operator+=(std::ptrdiff_t x) { n += x; return *this; } bitset_iterator operator-(std::ptrdiff_t x) const { return bitset_iterator(*B, n - x); } bitset_iterator& operator-=(std::ptrdiff_t x) { n -= x; return *this; } public: friend bool operator==(bitset_iterator x, bitset_iterator y) { return x.B == y.B && x.n == y.n; } friend bool operator!=(bitset_iterator x, bitset_iterator y) { return !(x == y); } friend bool operator<(bitset_iterator x, bitset_iterator y) { return x.n < y.n; } friend bool operator>(bitset_iterator x, bitset_iterator y) { return y < x; } friend bool operator<=(bitset_iterator x, bitset_iterator y) { return !(y < x); } friend bool operator>=(bitset_iterator x, bitset_iterator y) { return !(x < y); } friend std::ptrdiff_t operator-(bitset_iterator x, bitset_iterator y) { return x.n - y.n; } friend bitset_iterator operator+(std::ptrdiff_t n1, bitset_iterator x) { return bitset_iterator(*x.B, x.n + n1); } }; template bitset_iterator begin(const std::bitset return bitset_iterator } template bitset_iterator end(const std::bitset return bitset_iterator } template bitset_iterator begin(std::bitset return bitset_iterator } template bitset_iterator end(std::bitset return bitset_iterator } — End of Listing —vector
总结
Listing 1 - bitset_iterator, an iterator adaptor class for std::bitset