c++ 使用不触发UB的reinterpret_cast的示例

zvokhttg  于 2023-06-07  发布在  其他
关注(0)|答案(3)|浏览(177)

阅读https://en.cppreference.com/w/cpp/language/reinterpret_cast时,我想知道reinterpret_cast的哪些用例不是UB,而是在实践中使用的?
上面的描述包含了许多情况,其中将指针转换为其他类型然后返回是合法的。但这似乎不太实用。除了通过char*/byte*指针访问对象外,通过reinterpret_cast指针访问对象大多数情况下都是UB,因为违反了严格别名(和/或对齐)。
一个有用的例外是将整数常量转换为指针并访问目标对象,这对于处理HW寄存器(以µC为单位)很有用。
有谁能告诉我们一些reinterpret_cast在实践中的相关性的真实的用例吗?

ubby3x7f

ubby3x7f1#

我想到了一些例子:

  • 阅读/写一个普通可复制对象的对象表示,例如将对象的字节表示写入文件并读回:
// 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));

从技术上讲,除了直接传递指向memcpy et的指针之外,目前还没有真正指定如何访问对象表示。al,但目前有一个标准提案,至少阐明了在对象表示中阅读(而不是写入)单个字节应该如何工作,请参见https://github.com/cplusplus/papers/issues/592

  • 重新解释相同整数类型的有符号和无符号变体,特别是字符串的charunsigned char,如果API需要无符号字符串,这可能很有用。
auto str = "hello world!";
auto unsigned_str = reinterpret_cast<const unsigned char*>(str);

虽然这是别名规则所允许的,但从技术上讲,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)));

只要地址buf + sizeof(T)T适当对齐,缓冲区的类型为std::byteunsigned char,并且显然具有足够的大小,这种方法就可以工作。new表达式也返回一个指向对象的指针,但可能不希望为每个对象存储该指针。如果缓冲区中存储的所有对象都是同一类型,那么在单个这样的指针上使用指针算法也是可以的。

  • 获取指向特定内存地址的指针。这是否以及对于哪些地址值是可能的是实现定义的,就像结果指针的任何可能的使用一样:
auto ptr = reinterpret_cast<void*>(0x12345678);
  • dlsym(或类似函数)返回的void*转换为位于该地址的函数的实际类型。这是否可能以及语义究竟是什么再次由实现定义:
// 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();
  • 各种往返转换对于存储指针值或类型擦除可能是有用的。

一个对象指针通过void*的往返只需要两边都有static_cast,而对象指针上的reinterpret_cast是按照两步static_cast通过(cv限定的)void*来定义的。
一个对象指针通过std::uintptr_tstd::intptr_t或另一个足够大的整数类型来保存所有指针值的往返行程,对于拥有一个可以序列化的指针值的表示可能是有用的(尽管我不确定这有多有用)。然而,这些类型是否存在是由实现定义的。通常情况下,它们会这样做,但是标准允许使用一些特殊的平台,其中内存地址不能表示为单个整数值,或者所有整数类型都太小,无法覆盖地址空间。我也会对编译器的指针分析造成不同的问题,这取决于你如何使用它,见e.g. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65752是我发现的第一个bug报告。该标准并没有特别清楚地说明integer -> pointer强制转换应该如何工作,特别是在考虑指针出处时。参见例如https://open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2318r1.pdf和其中链接的其他文档。
函数指针在任意函数指针类型(可能是void(*)())之间的往返可能有助于从任意函数中删除该类型,尽管我也不确定这是否真的有用。void*类型擦除参数在C API中很常见,当函数只是传递数据时,但类型擦除函数指针就不那么常见了。
通过void*的函数指针的往返转换可以以与上述类似的方式使用,因为dlsym本质上与附加的动态库复杂性一起使用。这是有条件支持的,尽管它实际上是POSIX系统所必需的。(通常不支持,因为对象和函数指针值可能具有不同的表示,大小,对齐等。在一些更奇特的平台上)。

nimxete2

nimxete22#

使用reinterpret_cast的另一个现实示例是使用接受struct sockaddr *参数的各种网络相关函数,即recvfrom()bind()accept()
例如,下面是recvfrom函数的定义:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

它的第五个参数被定义为struct sockaddr *src_addr,它充当一个通用接口,用于接受指向特定地址类型(例如sockaddr_insockaddr_in6)的结构的指针。
The Beej's Guide to Network Programming说道:
在内存中,结构体sockaddr_in和结构体sockaddr_in6与结构体sockaddr共享相同的开始结构,并且您可以自由地将一种类型的指针强制转换为另一种类型,而不会造成任何伤害,除非可能出现宇宙的尽头。
关于宇宙终结的事情只是开个玩笑......如果当你将一个结构体sockaddr_in* 转换为一个结构体sockaddr* 时宇宙确实终结了,我向你保证这纯粹是巧合,你甚至不应该担心它。
所以,记住这一点,记住每当一个函数说它需要一个结构体sockaddr* 时,你可以轻松安全地将结构体sockaddr_in*、结构体sockaddr_in6* 或结构体sockadd_storage* 转换为该类型。
例如:

int fd; // file descriptor value obtained elsewhere
struct sockaddr_in addr {};
socklen_t addr_len = sizeof(addr);
std::vector<std::uint8_t> buffer(4096);
    
const int bytes_recv = recvfrom(fd, buffer.data(), buffer.size(), 0,
                                reinterpret_cast<sockaddr*>(&addr), &addr_len);
7z5jn7bk

7z5jn7bk3#

最常见的情况下,使用reinterpret_cast没有未定义的行为涉及方言,这些方言通过 * 指定 * 它们在比标准规定的更多情况下的行为来扩展 C++ 。* 定义 * 行为)。虽然C标准允许实现将“违反”别名规则的程序视为错误,但标准并不要求这样的程序被视为错误。根据C标准本身:
虽然本文档只声明了对C++实现的要求,但如果将这些要求表述为对程序、程序的一部分或程序的执行的要求,那么这些要求通常更容易理解。如果一个程序违反了一个不需要诊断的规则,本文档对该程序的实现不作要求
几乎所有实际的实现都可以配置为通过以与所涉及的对象的表示一致的方式处理reinterpret_cast来扩展语言的语义,而不考虑标准是否要求它们这样做。reinterpret_cast允许使用这种扩展的非可移植构造具有一致的语法,这一事实比使用该构造的大多数“可移植”方式更广泛地有用。

相关问题