Chapter18 std::byte

通过std::byte,C++17引入了一个类型来代表内存的最小单位:字节。 std::byte本质上代表一个字节的值,但并不能进行数字或字符的操作, 也不对每一位进行解释。对于不需要数字计算和字符序列的场景,这样会更加类型安全。

然而,注意std::byte实现的大小和unsigned char一样,这意味着 它并不保证是8位,可能会更多。

18.1 使用std::byte

下面的代码展示了std::byte的核心能力:

  1. #include <cstddef> // for std::byte
  2. std::byte b1{0x3F};
  3. std::byte b2{0b1111'0000};
  4. std::byte b3[4] {b1, b2, std::byte{1}}; // 4个字节(最后一个是0)
  5. if (b1 == b3[0]) {
  6. b1 <<= 1;
  7. }
  8. std::cout << std::to_integer<int>(b1) << '\n'; // 输出:126

这里,我们定义了两个初始值不同的字节。b2的初始化使用了两个C++14引入的特性:

  • 前缀0b允许定义二进制字面量
  • 数字分隔符 '可以增强数字字面量的可读性 (它可以被放置在数字字面量中任意两个数字之间)。

注意列表初始化(使用花括号初始化)是唯一可以直接初始化std::byte对象的方法。 所有其他的形式都不能编译:

  1. std::byte b1{42}; // OK(因为自从C++17起所有枚举都有固定的底层类型)
  2. std::byte b2(42); // ERROR
  3. std::byte b3 = 42; // ERROR
  4. std::byte b4 = {42}; // ERROR

这是将std::byte实现为枚举类型的一个直接后果。 花括号初始化使用了新的用整数值初始化有作用域的枚举特性。

也没有隐式类型转换,这意味着你必须显式对整数值进行转换才能初始化字节数组:

  1. std::byte b5[] {1}; // ERROR
  2. std::byte b6[] {std::byte{1}}; // OK

如果没有初始化,std::byte对象的值将是未定义的,因为它存储在栈上:

  1. std::byte b; // 值未定义

像通常一样(除了原子类型),你可以使用花括号强制初始化为每一位为0:

  1. std::byte b{}; // 等价于b{0}

std::to_integer<>允许你将std::byte对象转换为整数值(包括boolchar类型)。如果没有转换,将不能使用输出运算符。注意因为这个转换函数是模板, 所以你需要使用带有std::的完整名称:

  1. std::cout << b1; // ERROR
  2. std::cout << to_integer<int>(b1); // ERROR(ADL在这里不起作用)
  3. std::cout << std::to_integer<int>(b1); // OK

也可以使用using声明(但请只在局部作用域中这么做):

  1. using std::to_integer;
  2. ...
  3. std::cout << to_integer<int>(b1); // OK

如果要将std::byte用作bool值也需要这样的转换。例如:

  1. if (b2) ... // ERROR
  2. if (b2 != std::byte{0}) ... // OK
  3. if (to_integer<bool>(b2)) ... // ERROR(ADL在这里不起作用)
  4. if (std::to_integer<bool>(b2)) ... // OK

因为std::byte被实现为底层类型是unsigned char的枚举类型, 所以它的大小总是1:

  1. std::cout << sizeof(b); // 总是1

它的位数依赖于底层类型unsigned char的位数, 你可以通过标准数字限制来获取位数:

  1. std::cout << std::numeric_limits<unsigned char>::digits; // std::byte的位数

这等价于:

  1. std::cout << std::numeric_limits<std::underlying_type_t<std::byte>>::digits;

大多数时候结果是8,但在有些平台上可能不是。

18.2 std::byte类型和操作

这一节详细描述std::byte类型和操作。

18.2.1 std::byte类型

在头文件<cstddef>中,C++标准库以如下方式定义了std::byte

  1. namespace std {
  2. enum class byte : unsigned char {
  3. };
  4. }

也就是说,std::byte不是别的,只是一个带有一些位运算符操作的有作用域的枚举类型:

  1. namespace std {
  2. ...
  3. template<typename IntType>
  4. constexpr byte operator<< (byte b, IntType shift) noexcept;
  5. template<typename IntType>
  6. constexpr byte& operator<<= (byte& b, IntType shift) noexcept;
  7. template<typename IntType>
  8. constexpr byte operator>> (byte b, IntType shift) noexcept;
  9. template<typename IntType>
  10. constexpr byte& operator>>= (byte& b, IntType shift) noexcept;
  11. constexpr byte& operator|= (byte& l, byte r) noexcept;
  12. constexpr byte operator| (byte l, byte r) noexcept;
  13. constexpr byte& operator&= (byte& l, byte r) noexcept;
  14. constexpr byte operator& (byte l, byte r) noexcept;
  15. constexpr byte& operator^= (byte& l, byte r) noexcept;
  16. constexpr byte operator^ (byte l, byte r) noexcept;
  17. constexpr byte operator~ (byte b) noexcept;
  18. template<typename IntType>
  19. constexpr IntType to_integer (byte b) noexcept;
  20. }

18.2.2 std::byte操作

std::byte的操作列出了std::byte的所有操作。

操作 效果
构造函数 创建一个字节对象(调用默认构造函数时值未定义)
析构函数 销毁一个字节对象(什么也不做)
= 赋予新值
==、!=、<、<=、>、>= 比较字节对象
<<、>>、|、&、^、~ 二元位运算符
<<=、>>=、 |=、 &=、^= 修改自身的位运算符
to_integer<T>() 把字节对象转换为整数类型T
sizeof() 返回1

转换为整数类型

to_integer<>()可以把std::byte转换为任意基本整数类型 (bool、字符类型或者整数类型)。这也是必须的, 例如为了将std::byte和整数值比较或者将它用作条件:

  1. if (b2) ... // ERROR
  2. if (b2 != std::byte{0}) ... // OK
  3. if (to_integer<bool>(b2)) ... // ERROR(ADL在这里不生效)
  4. if (std::to_integer<bool>(b2)) ... // OK

另一个使用它的例子是std::byte I/O。

to_integer<>()使用static_cast来把unsigned char 转换为目标类型。例如:

  1. std::byte ff{0xFF};
  2. std::cout << std::to_integer<unsigned int>(ff); // 255
  3. std::cout << std::to_integer<int>(ff); // 也是255(没有负值)
  4. std::cout << static_cast<int>(std::to_integer<signed char>(ff)); // -1

std::byte的I/O

std::byte没有定义输入和输出运算符,因此不得不把它转换为整数类型再进行I/O:

  1. std::byte b;
  2. ...
  3. std::cout << std::to_integer<int>(b); // 以十进制值打印出值
  4. std::cout << std::hex << std::to_integer<int>(b); // 以十六进制打印出值

通过使用std::bitset<>,你可以以二进制输出值(一串位序列):

  1. #include <cstddef> // for std::byte
  2. #include <bitset> // for std::bitset
  3. #include <limits> // for std::numeric_limits
  4. std::byte b1{42};
  5. using ByteBitset = std::bitset<std::numeric_limits<unsigned char>::digits>;
  6. std::cout << ByteBitset{std::to_integer<unsigned>(b1)};

上例中using声明定义了一个位数和std::byte相同的bitset类型, 之后把字节对象转换为整数来初始化一个这种类型的对象,最后输出了该对象。 最后值42会以如下方式输出(假设一个char是8位):

  1. 00101010

另外,你可以使用std::underlying_type_t<std::byte>代替unsigned char, 这样using声明的目的将更明显。

你也可以使用这种方法把std::byte的二进制表示写入一个字符串:

  1. std::string s = ByteBitset{std::to_integer<unsigned>(b1)}.to_string();

如果你已经有了一个字符序列,你也可以像下面这样 使用byte到位序列

  1. #include <charconv>
  2. #include <cstddef>
  3. std::byte b1{42};
  4. // 译者注:此处原文写的是
  5. // int value = 42;
  6. // 应是作者笔误
  7. char str[100];
  8. std::to_chars_result res = std::to_chars(str, str+99, std::to_integer<int>(b1), 2);
  9. *res.ptr = '\0'; // 确保最后有一个空字符结尾

注意这种形式将不会写入前导0,这意味着对于值42,最后的结果是(假设一个char有8位):

  1. 101010
  2. // 译者注:此处原文写的是
  3. // 1111110
  4. // 应是作者笔误

可以使用相似的方式进行输入:以整数、字符串或bitset类型读入并进行转换。 例如,你可以像下面这样实现读入字节对象的二进制表示的输入运算符:

  1. std::istream& operator>> (std::istream& strm, std::byte& b)
  2. {
  3. // 读入一个bitset:
  4. std::bitset<std::numeric_limits<unsigned char>::digits> bs;
  5. strm >> bs;
  6. // 如果没有失败就转换为std::byte:
  7. if (!std::cin.fail()) {
  8. b = static_cast<std::byte>(bs.to_ulong()); // OK
  9. }
  10. return strm;
  11. }

注意我们必须使用static_cast<>()来把bitset转换成的unsigned long转换 为std::byte。列表初始化将不能工作,因为会发生窄化:

  1. b = std::byte{bs.to_ulong()}; // ERROR:发生窄化

并且我们也没有其他的初始化方法了。

另外,你也可以使用std::from_chars()来从给定的字符序列 读取:

  1. #include <charconv>
  2. const char* str = "101001";
  3. int value;
  4. std::from_chars_result res = std::from_chars(str, str+6, // 要读取的字符范围
  5. value, // 读取后存入的对象
  6. 2); // 2进制