std::byte
通过std::byte
,C++17
引入了一个类型来代表内存的最小单位:字节。std::byte
本质上代表一个字节的值,但并不能进行数字或字符的操作,也不对每一位进行解释。对于不需要数字计算和字符序列的场景,这样会更加类型安全。
然而,注意std::byte
实现的大小和unsigned char
一样,这意味着它并不保证是8位,可能会更多。
std::byte
:它代表了寻址单位,但没有作为整数类型的功能。std::byte
可以进行位运算,但不能够像char
那样进行普通代数运算。在提高了类型安全性的同时,也有助于提高程序的可读性。
You might wonder what the difference is with just using the existing uint8_t instead of std::byte. Well, std::byte is really just a bunch of un-interpreted bits. If you use uint8_t, you are actually interpreting the bits as an 8-bit unsigned numerical value, which might convey the wrong semantics. Also, std::byte will not allow accidental arithmetic on it, while uint8_t does.
std::byte
是一组未解释的位,而uint8_t
则将这些位解释为8位无符号数值。使用std::byte
可以避免错误的语义,并防止意外进行算术运算。
std::byte
是一种独立类型,它实现指定于C++
语言定义中的字节的概念。
同char
与unsigned char
,它能用于访问其他对象所占据的生内存(对象表示),但不同于这些类型,它不是字符类型且非算术类型。byte
只是位的汇集,而且只对它定义逐位运算符。
程序中许多需要对内存进行字节级别的访问。目前,这些程序必须使用
char
、signed char
或unsigned char
类型来实现。然而,这些类型执行“三重任务”。它们不仅用于字节寻址,还用作算术类型和字符类型。这种角色的多样性为程序员错误(例如在应将其视为字节值的内存上意外执行算术运算)和混淆打开了大门。
byte
类型的引入提高了类型安全性,通过区分基于字节的内存访问和将内存作为字符或整数值进行访问。它提高了可读性,并使代码意图更清晰地传达给读者(以及用于理解和转换程序的工具)。它通过消除程序员意图表达中的歧义,增加了表达式准确度,从而提高了类型安全性并增加了分析工具的精度。
使用std::byte
std::byte
是一种独立的类型,不属于整数或字符类型。它用于访问构成对象存储的位,以提高程序安全性。
下面的代码展示了std::byte
的核心能力:
#include <cstddef> // for std::byte
#include <iostream>
using namespace std;
int main() {
byte b1{0x3F};
byte b2{0b1111'0000};
cout << to_integer<int>(b1) << '\n'; // 输出: 63
byte b3[4]{b1, b2, std::byte{1}}; // 4个字节(最后一个是0)
if (b1 == b3[0]) {
b1 <<= 1;
}
cout << to_integer<int>(b1) << '\n'; // 输出:126
return 0;
}
预处理代码如下:
std::byte b1 = {63};
std::byte b2 = {240};
std::operator<<(std::cout.operator<<(std::to_integer<int>(b1)), '\n');
std::byte b3[4] = {b1, b2, std::byte{1}, 0};
if(b1 == b3[0]) {
std::operator<<=(b1, 1);
}
std::operator<<(std::cout.operator<<(std::to_integer<int>(b1)), '\n');
这里,我们定义了两个初始值不同的字节。b2
的初始化使用了两个C++14
引入的特性:
- 前缀
0b
允许定义二进制字面量 - 数字分隔符
'
可以增强数字字面量的可读性(它可以被放置在数字字面量中任意两个数字之间)。
注意列表初始化(使用花括号初始化)是唯一可以直接初始化std::byte
对象的方法。所有其他的形式都不能编译:
std::byte b1{42}; // OK(因为自从C++17起所有枚举都有固定的底层类型)
std::byte b2(42); // ERROR
std::byte b3 = 42; // ERROR
std::byte b4 = {42}; // ERROR
这是将std::byte
实现为枚举类型的一个直接后果。花括号初始化使用了新的用整数值初始化有作用域的枚举特性。
也没有隐式类型转换,这意味着你必须显式对整数值进行转换才能初始化字节数组:
std::byte b5[] {1}; // ERROR
std::byte b6[] {std::byte{1}}; // OK
如果没有初始化,std::byte
对象的值将是未定义的,因为它存储在栈上:
std::byte b; // 值未定义
像通常一样(除了原子类型),你可以使用花括号强制初始化为每一位为0:
std::byte b{}; // 等价于b{0}
std::to_integer<>
允许你将std::byte
对象转换为整数值(包括bool
和char
类型)。如果没有转换,将不能使用输出运算符。注意因为这个转换函数是模板,所以你需要使用带有std::
的完整名称:
std::cout << b1; // ERROR
std::cout << to_integer<int>(b1); // ERROR(ADL在这里不起作用)
std::cout << std::to_integer<int>(b1); // OK
也可以使用using
声明(但请只在局部作用域中这么做):
using std::to_integer;
...
std::cout << to_integer<int>(b1); // OK
如果要将std::byte
用作bool
值也需要这样的转换。例如:
if (b2) ... // ERROR
if (b2 != std::byte{0}) ... // OK
if (to_integer<bool>(b2)) ... // ERROR(ADL在这里不起作用)
if (std::to_integer<bool>(b2)) ... // OK
因为std::byte
被实现为底层类型是unsigned char
的枚举类型,所以它的大小总是1:
std::cout << sizeof(b); // 总是1
它的位数依赖于底层类型unsigned char
的位数,你可以通过标准数字限制来获取位数:
std::cout << std::numeric_limits<unsigned char>::digits; // std::byte的位数
这等价于:
std::cout << std::numeric_limits<std::underlying_type_t<std::byte>>::digits;
大多数时候结果是8,但在有些平台上可能不是。
std::byte
类型和操作
std::byte
类型
在头文件<cstddef>
中,C++
标准库以如下方式定义了std::byte
:
namespace std {
enum class byte : unsigned char {
};
}
也就是说,std::byte
不是别的,只是一个带有一些位运算符操作的有作用域的枚举类型:
namespace std {
...
template<typename IntType>
constexpr byte operator<< (byte b, IntType shift) noexcept;
template<typename IntType>
constexpr byte& operator<<= (byte& b, IntType shift) noexcept;
template<typename IntType>
constexpr byte operator>> (byte b, IntType shift) noexcept;
template<typename IntType>
constexpr byte& operator>>= (byte& b, IntType shift) noexcept;
constexpr byte& operator|= (byte& l, byte r) noexcept;
constexpr byte operator| (byte l, byte r) noexcept;
constexpr byte& operator&= (byte& l, byte r) noexcept;
constexpr byte operator& (byte l, byte r) noexcept;
constexpr byte& operator^= (byte& l, byte r) noexcept;
constexpr byte operator^ (byte l, byte r) noexcept;
constexpr byte operator~ (byte b) noexcept;
template<typename IntType>
constexpr IntType to_integer (byte b) noexcept;
}
std::byte
操作
表std::byte
的操作列出了std::byte
的所有操作。
操作 | 效果 |
---|---|
构造函数 | 创建一个字节对象(调用默认构造函数时值未定义) |
析构函数 | 销毁一个字节对象(什么也不做) |
= | 赋予新值 |
==、!=、<、<=、>、>= | 比较字节对象 |
<<、>>、 |、&、^、~ | 二元位运算符 |
<<=、>>=、 |=、 &=、^= | 修改自身的位运算符 |
to_integer<T>() | 把字节对象转换为整数类型T |
sizeof() | 返回1 |
#include <bitset>
#include <cstddef>
#include <iostream>
std::ostream& operator<<(std::ostream& os, std::byte b) {
return os << std::bitset<8>(std::to_integer<int>(b));
}
int main() {
std::byte b{42};
std::cout << "1. " << b << '\n';
// b *= 2 compilation error
b <<= 1;
std::cout << "2. " << b << '\n';
b >>= 1;
std::cout << "3. " << b << '\n';
std::cout << "4. " << (b << 1) << '\n';
std::cout << "5. " << (b >> 1) << '\n';
b |= std::byte{0b11110000};
std::cout << "6. " << b << '\n';
b &= std::byte{0b11110000};
std::cout << "7. " << b << '\n';
b ^= std::byte{0b11111111};
std::cout << "8. " << b << '\n';
}
运行结果如下:
1. 00101010
2. 01010100
3. 00101010
4. 01010100
5. 00010101
6. 11111010
7. 11110000
8. 00001111
转换为整数类型
用to_integer<>()
可以把std::byte
转换为任意基本整数类型(bool
、字符类型或者整数类型)。这也是必须的,例如为了将std::byte
和整数值比较或者将它用作条件:
if (b2) ... // ERROR
if (b2 != std::byte{0}) ... // OK
if (to_integer<bool>(b2)) ... // ERROR(ADL在这里不生效)
if (std::to_integer<bool>(b2)) ... // OK
另一个使用它的例子是std::byte
I/O。to_integer<>()
使用static_cast
来把unsigned char
转换为目标类型。例如:
std::byte ff{0xFF};
std::cout << std::to_integer<unsigned int>(ff); // 255
std::cout << std::to_integer<int>(ff); // 也是255(没有负值)
std::cout << static_cast<int>(std::to_integer<signed char>(ff)); // -1
std::byte
的I/O
std::byte
没有定义输入和输出运算符,因此不得不把它转换为整数类型再进行I/O:
std::byte b;
...
std::cout << std::to_integer<int>(b); // 以十进制值打印出值
std::cout << std::hex << std::to_integer<int>(b); // 以十六进制打印出值
通过使用std::bitset<>
,你可以以二进制输出值(一串位序列):
#include <cstddef> // for std::byte
#include <bitset> // for std::bitset
#include <limits> // for std::numeric_limits
std::byte b1{42};
using ByteBitset = std::bitset<std::numeric_limits<unsigned char>::digits>;
std::cout << ByteBitset{std::to_integer<unsigned>(b1)};
上例中using
声明定义了一个位数和std::byte
相同的bitset类型,之后把字节对象转换为整数来初始化一个这种类型的对象,最后输出了该对象。最后值42会以如下方式输出(假设一个char
是8位):
00101010
另外,你可以使用std::underlying_type_t<std::byte>
代替unsigned char
,这样using声明的目的将更明显。
你也可以使用这种方法把std::byte
的二进制表示写入一个字符串:
std::string s = ByteBitset{std::to_integer<unsigned>(b1)}.to_string();
如果你已经有了一个字符序列,你也可以像下面这样使用byte
到位序列
#include <bitset>
#include <charconv>
#include <cstddef>
#include <iostream>
int main() {
std::byte b1{42};
char str[100];
std::to_chars_result res =
std::to_chars(str, str + 99, std::to_integer<int>(b1), 2);
*res.ptr = '\0'; // 确保最后有一个空字符结尾
std::cout<< std::string(str); // 101010
}
注意这种形式将不会写入前导0,这意味着对于值42,最后的结果是(假设一个char
有8位):
101010
可以使用相似的方式进行输入:以整数、字符串或bitset
类型读入并进行转换。例如,你可以像下面这样实现读入字节对象的二进制表示的输入运算符:
std::istream& operator>> (std::istream& strm, std::byte& b)
{
// 读入一个bitset:
std::bitset<std::numeric_limits<unsigned char>::digits> bs;
strm >> bs;
// 如果没有失败就转换为std::byte:
if (!std::cin.fail()) {
b = static_cast<std::byte>(bs.to_ulong()); // OK
}
return strm;
}
注意我们必须使用static_cast<>()
来把bitset
转换成的unsigned long
转换为std::byte
。列表初始化将不能工作,因为会发生窄化:
b = std::byte{bs.to_ulong()}; // ERROR:发生窄化
并且我们也没有其他的初始化方法了。
另外,你也可以使用std::from_chars()
来从给定的字符序列读取:
#include <charconv>
const char* str = "101001";
int value;
std::from_chars_result res = std::from_chars(str, str+6, // 要读取的字符范围
value, // 读取后存入的对象
2); // 2进制