何时使用reinterpret_cast?

609
我对reinterpret_cast和static_cast的适用性有些困惑。从我所了解的来看,一般规则是当类型可以在编译时被解释时使用静态转换(static_cast),因此出现了"static"这个词。这是C++编译器在隐式转换时内部使用的转换。
reinterpret_cast有两种情况适用:
  • 将整数类型转换为指针类型或反之亦然
  • 将一个指针类型转换为另一个。我的一般理解是,这是不可移植的,应该避免使用。
我有一点困惑的是,我需要使用其中一个用法: 我正在从C中调用C++代码,而C代码需要保留C++对象,因此它保存了一个void*。应该使用何种转换方法来在void*与类类型之间进行转换?
我见过使用static_cast和reinterpret_cast的例子。尽管我所阅读的内容显示static_cast更好,因为转换可以在编译时发生,但它也说要使用reinterpret_cast将一个指针类型转换为另一个。

25
reinterpret_cast 没有发生在运行时,它们都是编译时语句。来自 http://en.cppreference.com/w/cpp/language/reinterpret_cast 的解释:与 static_cast 不同,但类似于 const_cast,reinterpret_cast 表达式不会编译为任何 CPU 指令。它纯粹是一个编译器指令,告诉编译器将表达式的位序列(对象表示)视为具有新类型。 - Cris Luengo
@HeretoLearn,能否添加*.c和*.cpp文件中相关的代码片段?我认为这可以提高问题的表达。 - OrenIshShalom
11个回答

563

C++标准保证以下内容:

将指针从void*进行static_cast转换后,地址会被保留。也就是说,在下面的代码中,abc都指向相同的地址:

int* a = new int();
void* b = static_cast<void*>(a);
int* c = static_cast<int*>(b);

reinterpret_cast 只保证当你将指针强制转换为另一种类型,然后再 reinterpret_cast 回原来的类型,你会得到原始值。因此在以下示例中:

int* a = new int();
void* b = reinterpret_cast<void*>(a);
int* c = reinterpret_cast<int*>(b);

ac包含相同的值,但b的值未指定。(实际上,它通常会包含与ac相同的地址,但这在标准中未指定,并且在具有更复杂内存系统的机器上可能不成立。)

对于从void*进行强制转换和强制转换到void*,应该优先使用static_cast


18
我喜欢'b'是未定义的这个事实。它可以防止你对它做一些愚蠢的事情。如果你将某些东西转换为另一个指针类型,你就会遇到问题,而你无法依赖它会使你更加小心谨慎。如果你在上面使用了static_cast<>,那么'b'有什么用呢? - Martin York
5
我认为reinterpret_cast<>保证了相同的位模式(这与另一种类型的有效指针不同)。 - Martin York
49
在使用reinterpret_cast时,C++11中的b值不再是未指定的。而在C++03中,使用reinterpret_castint*转换为void*是被禁止的(尽管编译器没有实现这一点并且也很不实用,因此在C++11中进行了更改)。 - Johannes Schaub - litb
118
这并没有真正回答“何时使用reinterpret_cast”的问题。 - einpoklum
11
@LokiAstari 我认为未指定并不能阻止你做傻事,只有当你记起未指定时才会阻止你。这是一个巨大的区别。就我个人而言,我不喜欢未指定。需要记住的东西太多了。 - Helin Wang
显示剩余8条评论

211

reinterpret_cast必要的一种情况是在与不透明数据类型进行接口时。这在供应商API中经常发生,程序员对其没有控制权。以下是一个人为制造的例子,其中供应商提供了用于存储和检索任意全局数据的API:

// vendor.hpp
typedef struct _Opaque * VendorGlobalUserData;
void VendorSetUserData(VendorGlobalUserData p);
VendorGlobalUserData VendorGetUserData();

要使用此API,程序员必须将其数据强制转换为VendorGlobalUserData,然后再转回来。使用static_cast是行不通的,必须使用reinterpret_cast

// main.cpp
#include "vendor.hpp"
#include <iostream>
using namespace std;

struct MyUserData {
    MyUserData() : m(42) {}
    int m;
};

int main() {
    MyUserData u;

        // store global data
    VendorGlobalUserData d1;
//  d1 = &u;                                          // compile error
//  d1 = static_cast<VendorGlobalUserData>(&u);       // compile error
    d1 = reinterpret_cast<VendorGlobalUserData>(&u);  // ok
    VendorSetUserData(d1);

        // do other stuff...

        // retrieve global data
    VendorGlobalUserData d2 = VendorGetUserData();
    MyUserData * p = 0;
//  p = d2;                                           // compile error
//  p = static_cast<MyUserData *>(d2);                // compile error
    p = reinterpret_cast<MyUserData *>(d2);           // ok

    if (p) { cout << p->m << endl; }
    return 0;
}

以下是样例API的一种人为实现:

// vendor.cpp
static VendorGlobalUserData g = 0;
void VendorSetUserData(VendorGlobalUserData p) { g = p; }
VendorGlobalUserData VendorGetUserData() { return g; }

9
是的,这大概是我所能想到的reinterpret_cast唯一有意义的用途。 - jalf
13
可能有些晚了,但为什么供应商API不使用void*呢? - Xeo
29
他们不使用void*是因为这样会在编译时失去(某些)类型检查。 - jesup
4
“不透明”数据类型的一个实际用例是,当您想将API公开给C语言,但使用C++编写实现时。 ICU是这样做的库的一个示例,在欺骗检查器API中,您会处理 USpoofChecker* 类型的指针,其中 USpoofChecker 是一个空结构体。然而,在后台,每当您传递一个 USpoofChecker* 时,它都会经历 reinterpret_cast 转换成内部的C++类型。 - sffc
2
@yeputons 这就是为什么重新解释 struct_a*->void*->struct_a* 是被定义良好的原因。另一方面,struct_a*->void*->struct_b* 和直接的 atruct_a->struct_b* 则不是。 - Mariusz Jaskółka
显示剩余6条评论

191

简短回答: 如果你不知道reinterpret_cast是什么意思,就不要使用它。如果将来需要用到它,你会知道的。

详细回答:

让我们考虑基本数字类型。

例如,当你将int(12)转换为float (12.0f)时,处理器需要调用一些计算,因为这两个数字具有不同的位表示。这就是static_cast的作用。

另一方面,当你调用reinterpret_cast时,CPU不会调用任何计算。它只是像拥有另一种类型的内存中的一组位一样处理。因此,当你使用这个关键字将int*转换为float*时,新值(在指针解引用之后)与数学含义上的旧值无关(忽略读取该值是未定义行为的事实)。

请注意,在重新解释转换后读取或修改值往往是未定义行为。在大多数情况下,如果您想要实现某些数据的位表示,则应使用指向或引用std::byte(从C++17开始),这几乎总是合法的操作。其他“安全”的类型是charunsigned char,但我认为在现代C++中不应该将其用于此目的,因为std::byte具有更好的语义。

例如:确实,reinterpret_cast不可移植,因为一个原因-字节顺序(字节序)。但这通常是使用它的最佳原因。让我们想象一个例子:您必须从文件中读取二进制32位数字,并且您知道它是大端。您的代码必须是通用的,并且必须在大端(例如某些ARM)和小端(例如x86)系统上正常工作。所以你必须检查字节顺序。它在编译时是众所周知的,因此您可以编写constexpr函数:您可以编写一个函数来实现此目的:

/*constexpr*/ bool is_little_endian() {
  std::uint16_t x=0x0001;
  auto p = reinterpret_cast<std::uint8_t*>(&x);
  return *p != 0;
}

解释:在内存中,x的二进制表示可以是0000'0000'0000'0001(big-endian)或0000'0001'0000'0000(little-endian)。在将指针p下的字节重新解释转换后,它们分别可以是0000'00000000'0001。如果使用静态转换,则无论使用哪种字节序,它始终为0000'0001

编辑:

在第一个版本中,我让示例函数is_little_endian成为constexpr。它在最新的gcc(8.3.0)上编译正常,但标准说这是非法的。clang编译器拒绝编译它(这是正确的)。


4
好的例子!我会将short替换为uint16_t,将unsigned char替换为uint8_t,以使其更容易理解但不改变原意。 - Jan Turoň
3
@JanTuroň 是的,我们不能假设 short 在内存中占据了16位。已经更正。 - Mariusz Jaskółka
3
示例有误。在constexpr函数中不允许使用reinterpret_cast。 - Michael Veksler
1
首先,这段代码被最新的clang(7.0.0)和gcc(8.2.0)都拒绝了。不幸的是,我没有找到正式语言中的限制。我能找到的只有https://social.msdn.microsoft.com/Forums/vstudio/en-US/8937807a-5633-490c-9c94-b42dcd68a098/constexpr-allows-reinterpretcast-in-function-body?forum=vclanguage。 - Michael Veksler
3
更具体地说,https://en.cppreference.com/w/cpp/language/constant_expression(项目16)明确说明reinterpret_cast不能在常量表达式中使用。还请参阅https://github.com/cplusplus/draft/blob/master/papers/N3797.pdf(5.19常量表达式)第125-126页,其中明确排除了reinterpret_cast。然后,在* 7.1.5 constexpr限定符 的第5项(第146页)对于非模板、非默认constexpr函数......如果不存在任何参数值使得......可以是核心常量表达式(5.19)的一个求值子表达式,则程序是有错误的*。 - Michael Veksler
显示剩余9条评论

21
reinterpret_cast 的含义在 C++ 标准中没有定义。因此,在理论上,reinterpret_cast 可能会导致程序崩溃。实际上,编译器会试图做你所期望的事情,即将你传递的位解释为你进行强制类型转换的类型。如果你知道你要使用的编译器对 reinterpret_cast 做了什么,你可以使用它,但说它是“可移植”的是不正确的。
对于你描述的情况以及几乎任何你可能考虑使用 reinterpret_cast 的情况,你都可以使用 static_cast 或其他一些替代方法。标准中有这样的规定(§5.2.9),关于你可以期望 static_cast 的行为:
“指向 cv void 的 rvalue 可以被显式转换为对象类型的指针。被转换为“指向 cv void”的对象类型指针再转换回原始指针类型会保留其原始值。”
因此,对于你的用例,标准化委员会似乎明确地建议你使用 static_cast

7
并不完全会导致程序崩溃。标准对 reinterpret_cast 提供了一些保证,只是通常人们期望的保证并不多。 - jalf
2
只要使用得当,reinterpret_cast从A到B再到A是完全安全和定义良好的。但B的值是未指定的,如果你依赖B的值,可能会发生糟糕的事情。但是只要你按照标准允许的方式使用,强制转换本身就足够安全了。 ;) - jalf
61
哈哈,我怀疑 reinterpret_crash 可能真的会让你的程序崩溃。但是 reinterpret_cast 不会。 ;) - jalf
7
我在我的编译器上尝试运行了reinterpret_crash,但是它却拒绝进行编译。虽然编译器的 Bug 阻止了我崩溃重新解释程序,但是我不会放弃。我会立即报告这个 Bug! - paercebal
20
template<class T, U> T reinterpret_crash(U a) { return *(T*)nullptr; } 这段代码中的模板函数reinterpret_crash接受两个模板参数TU,并以一个参数a作为输入。函数返回空指针(T*)nullptr所解释的类型的内容,但由于空指针无法访问任何内存地址,因此函数会导致程序崩溃。 - user142019
显示剩余2条评论

11

reinterpret_cast的一个用途是,如果你想对(IEEE 754)浮点数应用按位操作。其中一个例子就是快速反平方根技巧:

https://en.wikipedia.org/wiki/Fast_inverse_square_root#Overview_of_the_code

它将浮点数的二进制表示视为整数,向右移位并从常数中减去,从而将指数减半并取反。转换回浮点数后,它经过Newton-Raphson迭代,使这个近似更加精确:

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking
    i  = 0x5f3759df - ( i >> 1 );               // what the deuce? 
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//  y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}

原本是用C语言编写的,所以使用了C类型转换,但类似的C++类型转换是reinterpret_cast。


1
错误:将类型为'int64_t {aka long long int}'的右值表达式无效转换为'type 'double&'。 reinterpret_cast<double&>((reinterpret_cast<int64_t&>(d) >> 1) + (1L << 61)) - http://ideone.com/6S4ijc - Orwellophile
3
标准规定这是未定义行为:http://en.cppreference.com/w/cpp/language/reinterpret_cast(在“类型别名”下)。 - Cris Luengo
如果我用memcpy替换所有的reinterpret_cast,这样还会有未定义行为吗? - sandthorn
1
@sandthorn:根据标准,这是UB,但如果它适用于您的架构,请不要担心。我认为这个技巧对于任何Intel架构的编译器都可以使用。在其他架构上,它可能无法按预期工作(甚至崩溃)--例如,浮点数和长整型可能存储在单独的内存区域中(虽然我不知道有任何这样的架构,这只是一个论点...)。memcpy肯定会使其合法化。 - Cris Luengo

5
这是 Avi Ginsburg 的一个变种程序,它清晰地展示了 reinterpret_cast 的特性,这个特性在 Chris Luengo、flodin 和 cmdLP 中提到:编译器将指向的内存位置视为新类型的对象。
#include <iostream>
#include <string>
#include <iomanip>
using namespace std;

class A
{
public:
    int i;
};

class B : public A
{
public:
    virtual void f() {}
};

int main()
{
    string s;
    B b;
    b.i = 0;
    A* as = static_cast<A*>(&b);
    A* ar = reinterpret_cast<A*>(&b);
    B* c = reinterpret_cast<B*>(ar);
    
    cout << "as->i = " << hex << setfill('0')  << as->i << "\n";
    cout << "ar->i = " << ar->i << "\n";
    cout << "b.i   = " << b.i << "\n";
    cout << "c->i  = " << c->i << "\n";
    cout << "\n";
    cout << "&(as->i) = " << &(as->i) << "\n";
    cout << "&(ar->i) = " << &(ar->i) << "\n";
    cout << "&(b.i) = " << &(b.i) << "\n";
    cout << "&(c->i) = " << &(c->i) << "\n";
    cout << "\n";
    cout << "&b = " << &b << "\n";
    cout << "as = " << as << "\n";
    cout << "ar = " << ar << "\n";
    cout << "c  = " << c  << "\n";
    
    cout << "Press ENTER to exit.\n";
    getline(cin,s);
}

这会导致输出结果像这样:
as->i = 0
ar->i = 50ee64
b.i   = 0
c->i  = 0

&(as->i) = 00EFF978
&(ar->i) = 00EFF974
&(b.i)   = 00EFF978
&(c->i)  = 00EFF978

&b = 00EFF974
as = 00EFF978
ar = 00EFF974
c  = 00EFF974
Press ENTER to exit.

可以看到,B对象首先作为B特定数据在内存中构建,然后是嵌入的A对象。 static_cast 正确返回嵌入的A对象的地址,由 static_cast 创建的指针正确地给出了数据字段的值。 由 reinterpret_cast 生成的指针将b的内存位置视为普通的A对象,因此当指针尝试获取数据字段时,它返回一些B特定的数据,就好像这是该字段的内容。 reinterpret_cast 的一个用途是将指针转换为无符号整数(当指针和无符号整数的大小相同时): int i; unsigned int u = reinterpret_cast<unsigned int>(&i);

2
这里除了最后一个示例之外,所有内容都是未定义行为;它只是一种(不可靠的)说明语言实现细节的手段,具有一定的趣味性。 - Davis Herring

3

2

首先,你有一些特定类型的数据,比如这里的int:

int x = 0x7fffffff://==nan in binary representation

如果您想要将同一变量作为其他类型(如浮点数)访问: 您可以选择以下方式:

float y = reinterpret_cast<float&>(x);

//this could only be used in cpp, looks like a function with template-parameters

或者

float y = *(float*)&(x);

//this could be used in c and cpp

简介:它意味着相同的内存被用作不同的类型。因此,您可以将浮点数的二进制表示转换为整数类型,如上所述,以获得浮点数。例如,0x80000000表示-0(尾数和指数为空,但符号位,即最高有效位,为1)。这也适用于双精度和长双精度。
优化:我认为reinterpret_cast在许多编译器中都进行了优化,而c-casting是通过指针算术运算完成的(值必须复制到内存中,因为指针无法指向CPU寄存器)。
注意:在两种情况下,在进行强制转换之前应将转换后的值保存在变量中!这个宏可能有帮助:
#define asvar(x) ({decltype(x) __tmp__ = (x); __tmp__; })

1
确实,“它意味着相同的内存被用作不同的类型”,但这仅限于特定的类型对。在您的示例中,从intfloat&reinterpret_cast形式是未定义行为。 - Mariusz Jaskółka
编译器在可能的情况下将memcpy优化为纯寄存器操作;强制转换很容易(但也是UB——如果值被使用——正如本页面上所指出的那样)。 - Davis Herring

1
template <class outType, class inType>
outType safe_cast(inType pointer)
{
    void* temp = static_cast<void*>(pointer);
    return static_cast<outType>(temp);
}

我尝试总结并编写了一个使用模板的简单安全转换。

请注意,此解决方案不能保证将指针转换为函数。


2
什么?为什么要费心呢?这正是在这种情况下reinterpret_cast已经做的事情:“对象指针可以显式转换为不同类型的对象指针。[72]当一个对象指针类型的_prvalue_ v 转换为对象指针类型“指向_cv_ T”,结果是static_cast<cv T*>(static_cast<cv void*>(v))。”-- N3797。 - underscore_d
关于 c++2003 标准,我找不到 reinterpret_cast 能够执行 static_cast<cv T*>(static_cast<cv void*>(v)) 的内容。 - Саша Зезюлинский
1
好的,没错,但我不关心13年前的版本,如果(很可能)可以避免,大多数程序员也不应该关心。除非另有规定,答案和评论应该真正反映最新的可用标准...在我看来。无论如何,我猜委员会觉得有必要在2003年之后明确添加这一点。(因为我IRC记得,在C++11中也是这样) - underscore_d
1
C++03 之前是 C++98。许多项目使用旧的 C++ 而不是可移植的 C++。有时你必须关心可移植性。例如,你必须支持相同的代码在 Solaris、AIX、HPUX 和 Windows 上运行。当涉及到编译器依赖和可移植性时,这是棘手的。因此,在你的代码中使用 reinterpret_cast 是引入可移植性问题的一个很好的例子。 - Саша Зезюлинский
如果像我一样,你乐意将自己限制在与最新版本的语言兼容的平台上,那么你的反对意见就没有意义了。 - underscore_d
你的 safe_cast 函数与 reinterpret_cast 做的事情完全相同:https://dev59.com/GMHqa4cB1Zd3GeqPuzZY#68137312。 - anton_rh

-9
快速回答:如果可以编译,请使用static_cast,否则请使用reinterpret_cast

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接