原始字节类型 - std::byte

1. std::byte 是什么?

在 C++17 之前,当我们需要处理“字节”级别的数据(例如网络协议、文件二进制流、硬件寄存器)时,我们通常只有两个选择,但它们都有缺陷:

  1. 使用 charunsigned char
    • 缺陷char 太聪明了。它是字符类型,不仅代表数值,还代表 ASCII 码。
    • 坑点std::cout << my_char 会打印出字符 A 而不是数值 65。如果想把它当成纯二进制数据用,很容易和文本处理混淆。
  2. 使用 uint8_tunsigned char 的别名来自头文件 <cstdint>):
    • 缺陷:它是一个整数类型。
    • 坑点:编译器和 IDE 会认为它是数字,允许你进行加减乘除 uint8_t a = 10; a = a + 5;。这在处理二进制位时往往会导致逻辑错误(我们通常只做位操作,很少把字节当数字加减)。

C++17 的解决方案是引入一个新的类型 std::byte用于在类型系统中明确表示字节数据。它不是字符,也不是整数,只是一个纯粹的字节表示。

  • 它不是字符(不会打印出乱码)。
  • 它不是整数(不能直接加减乘除)。
  • 它只代表一段内存中的 8 个比特

std::byte 实际上是基于 unsigned char 的强类型枚举,定义在头文件<cstddef> 中,它的底层实现类似于:

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

这带来了几个重要特性:

  1. 内存大小不变:它是 unsigned char,因此大小保证为 1 字节,能够填充内存的任意位模式。
  2. 类型安全:由于是 enum class,它不会隐式转换为 intchar,保证了类型安全。
  3. 明确意图:代码中看到 std::byte 就知道是在处理原始字节
  4. 限制操作,无算数运算:C++17 为它专门重载了位运算符
    • 按位或 |=, |
    • 按位与 &=, &
    • 按位异或 ^=, ^
    • 按位取反 ~
    • 移位 <<=, <<, >>=, >>

2. std::byte 怎么用

2.1 初始化 std::byte

由于 std::byte 是强类型的枚举类,我们不能直接用整数赋值(如果没有强制类型转换)需要使用 std::byte{}。当写下 std::byte{} 时,是在告诉编译器:“请构造一个 std::byte 类型的对象,并使用空列表 {} 对其进行初始化”。

  • 对于普通类/结构体MyClass{} 会调用默认构造函数。
  • 对于枚举类(如 std::byte):它不涉及传统意义上的构造函数,而是进行 零初始化

由于 {} 里面是空的,std::byte{} 会执行 值初始化,对于枚举类型来说,这等同于 零初始化。因为 std::byte 的底层类型是 unsigned char,所以:

  • std::byte{} 生成的值等于 static_cast<std::byte>(0)
  • 它的整数值为 0,二进制为 0000 0000

因此,std::byte{} 的含义是:创建一个初始值为 0 的 std::byte 类型的临时纯右值对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <cstddef>
#include <iostream>

int main()
{
// 1. 默认初始化(值未定义)
std::byte b1;
std::byte b2{};
std::byte b3{0x41};
std::byte b4{65};
std::byte b5{'A'}; // 字符 'A' 的 ASCII 码是 65,把它看作数值存进去
std::byte b6 = std::byte{0x01}; // 十六进制
std::byte b7 = std::byte{0b10101010}; // 二进制
std::byte b8 = std::byte{255}; // 十进制
// 错误:不能隐式转换
// std::byte b9 = 10;

return 0;
}

std::byte 把任何数字都视为比特模式的集合b3 虽然用了字符 A,但在 std::byte 眼里它就是 0100 0001 这串二进制,没有字符属性。

2.2 位运算操作

std::byte 不支持 +-*/ 等算术运算,位运算操作是它最强大的地方。它让代码明确表达了:“我在操作二进制位”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <cstddef>
#include <iostream>

// 位运算
void bitwise_operations()
{
std::byte a{0b00110011}; // 51
std::byte b{0b11110000}; // 240

// 位运算(返回 std::byte)
std::byte c = a & b; // 按位与: 0b00110000 (48)
std::byte d = a | b; // 按位或: 0b11110011 (243)
std::byte e = a ^ b; // 按位异或: 0b11000011 (195)
std::byte f = ~a; // 按位取反: 0b11001100 (204)

// 位移运算
std::byte g = a << 2; // 左移2位: 0b11001100 (204)
std::byte h = a >> 2; // 右移2位: 0b00001100 (12)
}

// 复合赋值
void compound_assignments()
{
std::byte x{0b10101010}; // 170

// 复合位运算
x &= std::byte{0b11110000}; // x = 0b10100000 (160)
x |= std::byte{0b00001111}; // x = 0b10101111 (175)
x ^= std::byte{0b11111111}; // x = 0b01010000 (80)

// 位移赋值
x <<= 1; // 左移一位: 0b10100000 (160)
x >>= 2; // 右移两位: 0b00101000 (40)
}

// 比较
void comparison_operations()
{
std::byte a{100};
std::byte b{200};
std::byte c{100};

// 比较运算(直接支持)
if (a == c) {
std::cout << "a equals c" << std::endl;
}

if (a != b) {
std::cout << "a not equals b" << std::endl;
}

// 所有比较操作符都支持
if (a < b) {
std::cout << "a is less than b" << std::endl;
}

if (b >= a) {
std::cout << "b is greater than or equal to a" << std::endl;
}
}

int main()
{
bitwise_operations();
compound_assignments();
comparison_operations();
return 0;
}

std::byte的比较运算是直接支持,不需要运算符重载。

2.3 把 std::byte 转回整数

std::byte 转换回整数是一个明确的操作,因为 std::byte 的设计初衷就是为了防止隐式的算术转换,所以你必须显式地进行转换。C++17 中可以使用 std::to_integer函数,这是标准库提供的唯一推荐且安全的方式。它是一个函数模板,定义在头文件 <cstdint> 中(通常包含 <cstddef> 也会间接包含,但建议主动引入)。

其函数原型如下:

1
2
template <class IntegerType>
constexpr IntegerType to_integer(byte b) noexcept;

我们可以将 std::byte 转换为任何整数类型(int, unsigned int, uint8_t 等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <cstddef>
#include <iostream>
#include <type_traits>

void to_integer_demo()
{
std::byte b{0xAB}; // 十六进制 171

// 转换为各种整数类型
auto as_int = std::to_integer<int>(b);
auto as_uint = std::to_integer<unsigned int>(b);
auto as_char = std::to_integer<char>(b);

std::cout << "as int: " << as_int << std::endl; // 171
std::cout << "as unsigned: " << as_uint << std::endl; // 171

// 注意:char 可能是有符号的
std::cout << "as char (numeric): "
<< (int)as_char << std::endl; // 需要强制转换

// 检查类型
static_assert(std::is_same_v<decltype(as_int), int>);
static_assert(std::is_same_v<decltype(as_uint), unsigned int>);
}

int main()
{
to_integer_demo();
return 0;
}

std::to_integer 的行为不仅仅是转换,它在概念上类似于 static_cast,它会对 std::byte 的底层数值进行整数转换。

  • 无符号目标类型(如 uint8_t, uint32_t):

    结果就是该字节代表的数值。例如 0xFFuint8_t 是 255,转 uint32_t 也是 255。

  • 有符号目标类型(如 int8_t, int):

    这里需要小心。如果 std::byte 的最高位是 1(例如 0xFF,即二进制 1111 1111):

    • 转换为 int8_t(有符号 char):在大多数采用补码的机器上,这会被解释为 -1
    • 转换为 int(通常是 32 位):根据整数转换规则,uint8_t 的值(255)会被提升为 32 位的整数 255

3. 案例: 简单的网络数据包处理

下面是一个关于 std::byte 的综合性示例。这个例子构建了一个模拟场景:简单的网络数据包处理。它涵盖了 std::byte 最核心的用途,包括:

  1. 类型转换:将强类型整数转换为字节。
  2. 位操作:使用位掩码提取和设置特定位(模拟协议标志位)。
  3. 按位移位:手动将大整数(如16位端口)拆解为字节流。
  4. 输出转换:将字节转回整数以便观察。

场景背景:我们要构建一个模拟的数据包头,格式要求如下:

  • Byte 0: [版本(3bit) | 标志(3bit) | 保留(2bit)]
  • Byte 1: 负载长度
  • Byte 2-3: 目标端口号 (16位, 大端序)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <iostream>
#include <vector>
#include <bitset>
#include <cstddef>
#include <cstdint>

// 辅助函数:以二进制和十进制打印 byte
void print_byte(std::byte b)
{
std::cout << "Raw: " << std::bitset<8>(std::to_integer<int>(b))
<< " (Int: " << std::to_integer<int>(b) << ")";
}

int main()
{
std::vector<std::byte> packet;

// ------------------------------------------------------
// 1. 构造第一个字节:位拼接与掩码操作
// ------------------------------------------------------
// 假设:版本 = 5 (二进制 101), 标志 = 3 (二进制 011)

std::byte version_part = std::byte{5}; // 0000 0101
std::byte flags_part = std::byte{3}; // 0000 0011

// 将版本移位到高 3 位 (左移 5 位)
std::byte header_byte = version_part << 5;

// 将标志放入中间 3 位 (左移 2 位)
header_byte |= (flags_part << 2);

// 添加到包中
packet.push_back(header_byte);

std::cout << "1. 构造 Header Byte:" << std::endl;
print_byte(packet[0]);
std::cout << "\n 预期: Ver(101) << 5 + Flags(011) << 2 = 10101100\n" << std::endl;


// ------------------------------------------------------
// 2. 构造负载长度 (普通转换)
// ------------------------------------------------------
uint8_t length = 64;
// 使用 std::to_byte 将整数转为字节
packet.push_back(std::byte{length});

std::cout << "2. 添加负载长度:" << std::endl;
print_byte(packet[1]);
std::cout << "\n" << std::endl;


// ------------------------------------------------------
// 3. 构造端口号:多字节拆解 (移位与掩码)
// ------------------------------------------------------
uint16_t port = 8080; // 二进制: 0001 1111 1000 0000

// 提取高 8 位 (右移 8 位)
std::byte port_high = std::byte{static_cast<unsigned char>((port >> 8) & 0xFF)};

// 提取低 8 位 (使用掩码 0xFF)
std::byte port_low = std::byte{static_cast<unsigned char>(port & 0xFF)};

packet.push_back(port_high);
packet.push_back(port_low);

std::cout << "3. 添加端口号 (Big Endian):" << std::endl;
std::cout << " Port: " << port << std::endl;
print_byte(port_high);
std::cout << " (高字节)" << std::endl;
print_byte(port_low);
std::cout << " (低字节)\n" << std::endl;


// ------------------------------------------------------
// 4. 解析数据包:读取与验证
// ------------------------------------------------------
std::cout << "=== 接收端解析数据包 ===" << std::endl;

// A. 读取并解析 Header (反向操作)
std::byte recv_header = packet[0];

// 提取版本:右移 5 位,然后掩码 0x07 (111)
int parsed_ver = std::to_integer<int>((recv_header >> 5) & std::byte{0x07});

// 提取标志:右移 2 位,然后掩码 0x07 (111)
int parsed_flags = std::to_integer<int>((recv_header >> 2) & std::byte{0x07});

std::cout << "解析版本: " << parsed_ver << std::endl;
std::cout << "解析标志: " << parsed_flags << std::endl;


// B. 重组端口号
// 先将 byte 转回整数,再通过移位组合
uint16_t reconstructed_port = (std::to_integer<uint16_t>(packet[2]) << 8) |
std::to_integer<uint16_t>(packet[3]);

std::cout << "解析端口: " << reconstructed_port << std::endl;

return 0;
}