提问者:小点点

使用不触发UB的reinterpret_cast的示例


阅读https://en.cppreference.com/w/cpp/language/reinterpret_cast我想知道reinterpret_cast的哪些用例不是UB并且在实践中使用?

上面的描述包含了许多情况,在这些情况下,将指针转换成其他类型是合法的,然后再转换回来也是合法的。但这似乎没什么实际用途。除了通过< code > char * /< code > byte * 指针访问之外,通过< code>reinterpret_cast指针访问对象大多是UB,因为违反了严格别名(和/或对齐)。

一个有用的例外是将整数常量强制转换为指针并访问目标对象,这对于操作硬件寄存器(以μC为单位)很有用。

有人能告诉我一些在实践中使用的reinterpret_cast相关性的真实用例吗?


共1个答案

匿名用户

想到的一些例子:

>

  • 读取/写入可简单复制的对象的对象表示形式,例如将对象的字节表示形式写入文件并将其读回:

    // 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等的指针。al,但是目前有一个标准提案,至少澄清了读取(而不是写入)对象表示中的单个字节应该如何工作,参见https://github.com/cplusplus/papers/issues/592.

    在C 20之前,<code>reinterpret_cast</code>是访问对象表示的唯一方法,但自从C 20之后,就有了<code>std::bit_cast>/code,这也允许它(具有相同的可复制要求),尽管它总是生成对象表示的副本。

    在同一整型的有符号和无符号变量之间重新解释,尤其是字符串的< code>char和< code>unsigned char,如果API需要无符号字符串,这可能会很有用。

    auto str = "hello world!";
    auto unsigned_str = reinterpret_cast<const unsigned char*>(str);
    

    虽然别名规则允许这样做,但从技术上讲,标准目前并未定义对结果< code>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::byte无符号字符,并且显然具有足够的大小,这就可以工作。表达式还返回指向对象的指针,但可能不希望为每个对象存储该指针。如果缓冲区中存储的所有对象都是同一类型,则在单个此类指针上使用指针算术也可以。

    获取指向特定内存地址的指针。这是否可行以及对于哪些地址值是实现定义的,结果指针的任何可能使用也是如此:

    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();
    

    这种使用是必需的,并且在使用 dlopen/dlsym 的动态加载工具时没有替代方法。

    各种往返转换对于存储指针值或类型擦除可能很有用。

    对象指针通过<code>void*</code>的往返只需要两侧的<code>static_cast</code>,而对象指针上的<code〕reinterpret_cast</code>是根据两步<code>到(cv限定)<code>void*定义的。

    对象指针通过std::uintptr_tstd::intptr_t,或其他大到足以容纳所有指针值的整数类型的往返,对于具有可以序列化的指针值表示可能很有用(尽管我不确定这是否真的有用)。然而,这些类型中是否存在是由实现定义的。通常它们会存在,但是标准允许内存地址不能表示为单个整数值或所有整数类型太小而无法覆盖地址空间的异国平台。

    通过任意函数指针类型(可能是<code>void(*)()</code>)往返函数指针可能有助于从任意函数中删除该类型,但我也不确定这是否真的有用void*类型擦除的参数在C API中很常见,当函数只通过数据时,但类型擦除的函数指针则不太常见。

    函数指针通过void*的往返转换可以以与上述类似的方式使用,就像dlsym本质上对额外的动态库复杂性所做的那样。这仅是有条件支持的,尽管POSIX系统实际上需要它。(它通常不受支持,因为在一些更奇特的平台上,对象和函数指针值可能有不同的表示、大小、对齐方式等。)