wim*_*aan 24 c++ strict-aliasing undefined-behavior reinterpret-cast
阅读https://en.cppreference.com/w/cpp/language/reinterpret_cast我想知道哪些用例reinterpret_cast不是 UB 并在实践中使用?
上面的描述包含了许多将指针转换为其他类型然后再转换回来是合法的情况。但这似乎不太实用。通过指针访问对象reinterpret_cast大多是 UB,因为违反了严格别名(和/或对齐),除了通过char*/byte*指针访问之外。
一个有用的例外是将整数常量转换为指针并访问目标对象,这对于操作硬件寄存器(在 \xc2\xb5C 中)很有用。
\n谁能告诉我们在实践中使用的reinterpret_cast相关性的一些真实用例吗?
\nuse*_*522 32
我想到的一些例子:
读/写可普通复制对象的对象表示形式,例如将对象的字节表示形式写入文件并将其读回:
// T must be trivially-copyable object type!
T obj;
//...
std::ofstream file(/*...*/);
file.write(reinterpret_cast<char*>(obj), sizeof(obj));
//...
std::ifstream file(/*...*/);
file.read(reinterpret_cast<char*>(obj), sizeof(obj));
Run Code Online (Sandbox Code Playgroud)
从技术上讲,除了直接将指针传递给 et 之外,目前还没有真正指定如何访问对象表示memcpy。al,但当前有一个标准提案至少澄清了如何读取(而不是写入)对象表示中的各个字节,请参阅https://github.com/cplusplus/papers/issues/592。
重新解释同一整数类型的有符号和无符号变体,特别char是unsigned char对于字符串,如果 API 需要无符号字符串,这可能很有用。
auto str = "hello world!";
auto unsigned_str = reinterpret_cast<const unsigned char*>(str);
Run Code Online (Sandbox Code Playgroud)
虽然别名规则允许这样做,但从技术上讲,unsigned_str标准当前尚未定义结果指针的指针算术。但我真的不明白为什么不是。
访问嵌套在字节缓冲区内的对象(尤其是在堆栈上):
alignas(T) std::byte buf[42*sizeof(T)];
new(buf+sizeof(T)) T;
// later
auto ptr = std::launder(reinterpret_cast<T*>(buf + sizeof(T)));
Run Code Online (Sandbox Code Playgroud)
只要地址buf + sizeof(T)适当对齐T,缓冲区的类型为std::byte或unsigned char,并且显然有足够的大小,这就可以工作。该new表达式还返回一个指向该对象的指针,但可能不希望为每个对象存储该指针。如果存储在缓冲区中的所有对象都是相同类型,那么对单个此类指针使用指针算术也是可以的。
获取指向特定内存地址的指针。是否以及对于哪些地址值可能是实现定义的,结果指针的任何可能使用也是如此:
auto ptr = reinterpret_cast<void*>(0x12345678);
Run Code Online (Sandbox Code Playgroud)
将void*by dlsym(或类似函数)返回的值转换为位于该地址的函数的实际类型。这是否可能以及语义到底是什么再次由实现定义:
// my_func is a C linkage function with type `void()` in `my_lib.so`
// error checking omitted!
auto lib = dlopen("my_lib.so", RTLD_LAZY);
auto my_func = reinterpret_cast<void(*)()>(dlsym(lib, "my_func");
my_func();
Run Code Online (Sandbox Code Playgroud)
各种往返转换对于存储指针值或类型擦除可能很有用。
对象指针的往返void*只需static_cast在两侧进行,并且reinterpret_cast对象指针无论如何都是根据两步static_cast通过(cv 限定)来定义的void*。
对象指针通过std::uintptr_t、std::intptr_t或另一个足够大以容纳所有指针值的整型类型的往返可能对于获得可序列化的指针值的表示很有用(尽管我不确定这真正有用的频率)。然而,这些类型是否存在是由实现定义的。通常它们会,但是标准允许内存地址不能表示为单个整数值或所有整数类型都太小而无法覆盖地址空间的奇异平台。我还会对编译器进行不同的指针分析,从而导致问题,具体取决于您如何使用它,请参阅https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65752作为我发现的第一个错误报告。该标准对于整数 -> 指针转换应该如何工作并不是特别清楚,特别是在考虑指针来源时。例如,请参阅https://open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2318r1.pdf以及其中链接的其他文档。
函数指针通过任何任意函数指针类型(可能)的往返void(*)()可能有助于从任意函数中删除类型,尽管我再次不确定这真正有用的频率。void*当函数仅传递数据时,类型擦除的参数在 C API 中很常见,但像这样的类型擦除的函数指针不太常见。
函数指针的往返转换void*可以以与上面类似的方式使用,dlsym本质上与附加动态库复杂性一样。尽管 POSIX 系统实际上需要这样做,但这仅是有条件支持的。(通常不支持它,因为在一些更奇特的平台上,对象和函数指针值可能具有不同的表示、大小、对齐方式等。)
VL-*_*-80 10
另一个现实世界的使用示例reinterpret_cast是使用接受参数的各种网络相关函数struct sockaddr *,即recvfrom()、bind()或accept()。
例如,以下是recvfrom函数的定义:
\nssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,\n struct sockaddr *src_addr, socklen_t *addrlen);\nRun Code Online (Sandbox Code Playgroud)\n它的第五个参数定义为struct sockaddr *src_addr,它充当通用接口,用于接受指向特定地址类型的结构的指针(例如,sockaddr_inor sockaddr_in6)。
Beej的网络编程指南说道:
\n\n\n在内存中,struct sockaddr_in 和 struct sockaddr_in6 与 struct sockaddr 共享相同的起始结构,并且您可以自由地将一种类型的指针转换为另一种类型,而不会造成任何损害,除了宇宙可能的终结。
\n只是在宇宙终结的事情上开玩笑\xe2\x80\xa6如果当你将 struct sockaddr_in* 转换为 struct sockaddr* 时宇宙确实结束了,我\n向你保证\xe2\x80\x99s 纯粹是巧合你甚至不应该担心\xe2\x80\x99。
\n因此,考虑到这一点,请记住,每当函数说它需要 struct sockaddr* 时,您都可以轻松安全地将 struct sockaddr_in*、struct\nsockaddr_in6* 或 struct sockadd_storage* 转换为该类型。
\n
例如:
\nssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,\n struct sockaddr *src_addr, socklen_t *addrlen);\nRun Code Online (Sandbox Code Playgroud)\n