【问题标题】:Reinterpret_cast vs. C-style castReinterpret_cast 与 C 风格的演员表
【发布时间】:2011-10-20 05:55:05
【问题描述】:

我听说reinterpret_cast 是实现定义的,但我不知道这究竟意味着什么。你能举个例子说明它是如何出错的,它出错了,使用 C-Style cast 更好吗?

【问题讨论】:

    标签: c++ casting


    【解决方案1】:

    C 风格的演员表并不好。

    它只是按顺序尝试各种 C++ 风格的强制转换,直到找到一个有效的强制转换。这意味着当它像reinterpret_cast 一样工作时,它的问题与reinterpret_cast 完全相同。但除此之外,它还有这些问题:

    • 它可以做很多不同的事情,并且从阅读代码中并不总是清楚将调用哪种类型的演员表(它可能表现得像 reinterpret_castconst_caststatic_cast,而那些非常不同的东西)
    • 因此,更改周围的代码可能会改变强制转换的行为
    • 阅读或搜索代码时很难找到 - reinterpret_cast 很容易找到,这很好,因为演员表很难看,使用时要注意。相反,通过搜索可靠地找到 C 风格的演员表(如 (int)42.0)要困难得多

    要回答您问题的另一部分,是的,reinterpret_cast 是实现定义的。这意味着当您使用它从int* 转换为float* 时,您无法保证生成的指针将指向相同的地址。那部分是实现定义的。但是,如果您将生成的float*reinterpret_cast 取回int*,那么您将获得原始指针。这部分是有保证的。

    但是,请记住,无论您使用 reinterpret_cast 还是 C 样式转换,这都是正确的:

    int i;
    int* p0 = &i;
    
    float* p1 = (float*)p0; // implementation-defined result
    float* p2 = reinterpret_cast<float*>(p0); // implementation-defined result
    
    int* p3 = (int*)p1; // guaranteed that p3 == p0
    int* p4 = (int*)p2; // guaranteed that p4 == p0
    int* p5 = reinterpret_cast<int*>(p1); // guaranteed that p5 == p0
    int* p6 = reinterpret_cast<int*>(p2); // guaranteed that p6 == p0
    

    【讨论】:

    • 如果我在不同的编译器或再次在同一个编译器上运行它,实现定义的结果会有所不同吗?这是否意味着代码不可移植?
    • 实现定义意味着实现(基本上是编译器)可以选择行为方式,但它必须记录行为。所以通常,这意味着如果您重新编译或再次运行程序,单个编译器将始终如一地执行相同的操作。但是,如果您使用不同的编译器进行编译,或者针对不同的 CPU,它可能会做其他事情(同样,必须记录在案)。所以是的,如果你依赖中间值,你的代码是不可移植的。如果你想便携,你必须做完整的往返
    • 我看到你展示的一些转换是有保证的,它是否取决于我们正在解释的对象的类型,例如 float* 到 int* 还是因为 reinterpret_cast 以这种方式工作。一些抽象类型呢?
    • @user974191 这就是我在上面的例子中试图说的。 reinterpret_castAB 会产生一个实现定义的结果,但 ABA 保证提供与您开始时相同的值。
    • @jalf 很好的答案,我在一个小时的谷歌搜索中找到了最好的答案。分享一下:Stroustrup 在“The C++ Programming Language”中提到A 的大小需要适合B 的大小,以保证ABA 的转换序列正常工作.将指针投射到非指针字符并返回显然会导致流泪。不太明显的是,在一些晦涩的硬件平台上指针的存储大小可能会根据指向的类型而有所不同,唯一的保证是 void* 需要能够适应其中的任何一个。
    【解决方案2】:

    它是在某种意义上定义的实现,标准没有(几乎)规定不同类型的值在位级别上的外观,地址空间的结构等等。所以它确实是一个非常特定于转换的平台,例如:

    double d;
    int &i = reinterpret_cast<int&>(d);
    

    但正如标准所说

    它旨在让那些知道寻址结构的人不足为奇 底层机器。

    因此,如果您知道自己在做什么以及在低级别上的外观如何,就不会出错。

    C 风格的强制转换在某种意义上有点相似,它可以执行 reinterpret_cast,但它也首先“尝试”static_cast,它可以抛弃 cv 限定(而 static_cast 和 reinterpret_cast 不能)并执行不考虑访问控制的转换(参见 C++11 标准中的 5.4/4)。例如:

    #include <iostream>
    
    using namespace std;
    
    class A { int x; };
    class B { int y; };
    
    class C : A, B { int z; };
    
    int main()
    {
      C c;
    
      // just type pun the pointer to c, pointer value will remain the same
      // only it's type is different.
      B *b1 = reinterpret_cast<B *>(&c);
    
      // perform the conversion with a semantic of static_cast<B*>(&c), disregarding
      // that B is an unaccessible base of C, resulting pointer will point
      // to the B sub-object in c.
      B *b2 = (B*)(&c);
    
      cout << "reinterpret_cast:\t" << b1 << "\n";
      cout << "C-style cast:\t\t" << b2 << "\n";
      cout << "no cast:\t\t" << &c << "\n";
    }
    

    这是 ideone 的输出:

    reinterpret_cast: 0xbfd84e78 C 风格转换:0xbfd84e7c 没有演员表:0xbfd84e78

    请注意,reinterpret_cast 产生的值与 'c' 的地址完全相同,而 C 风格的转换产生了正确的偏移指针。

    【讨论】:

    • 该标准为 reinterpret_cast 提供了几个保证和要求。最重要的是往返转换,指针到整数到指针会产生原始指针。此外,C++ 巩固了使用标准布局类型进行转换的预期行为。
    • @edA-qa mort-ora-y:假设整数大到足以容纳指针。最重要的一个(IMO)是从 A* -> void* -> A* 产生原始文件的往返。
    • @LokiAstari:当 A 是对象类型时 :) 无论如何,如果需要完整而精确的图片,最好看一下标准。
    • 对于void* (关于对象类型) 使用static_cast 的往返行程将起作用。在这种情况下,结果是相同的,但可能会阻止您意外转换为错误的类型。
    • @edA-qamort-ora-y:是的,它是 5.2.10/4 “指针可以显式转换为任何整数类型大到足以容纳它”5.2 .10/7 "pointer to T1" 转换为"pointer to cv T2" 类型,结果是 static_cast(static_cast(v))" 因此重新解释强制转换工作 也用于强制转换和来自 void*。更重要的是在我眼里。 reinterpret_cast 在代码中比 static_cast 更突出,因此受到更高程度的审查。
    【解决方案3】:

    使用reinterpret_cast 是有正当理由的,由于这些原因,标准实际上定义了会发生什么。

    第一种是使用不透明指针类型,用于库 API 或只是将各种指针存储在单个数组中(显然连同它们的类型一起)。您可以将指针转换为适当大小的整数,然后再转换回指针,它将是完全相同的指针。例如:

    T b;
    intptr_t a = reinterpret_cast<intptr_t>( &b );
    T * c = reinterpret_cast<T*>(a);
    

    在此代码中,c 保证如您预期的那样指向对象 b。转换回不同的指针类型当然是未定义的(有点)。

    函数指针和成员函数指针允许进行类似的转换,但在后一种情况下,您可以转换为/从另一个成员函数指针转换为具有足够大的变量。

    第二种情况是使用标准布局类型。这是 C++11 之前事实上支持的东西,现在已在标准中指定。在这种情况下,标准首先将 reinterpret_cast 视为 static_cast 到 void*,然后是 static_cast 到目标类型。这在执行二进制协议时经常使用,其中数据结构通常具有相同的标头信息,并允许您转换具有相同布局但 C++ 类结构不同的类型。

    在这两种情况下,您都应该使用显式的reinterpret_cast 运算符而不是C 样式。虽然 C 风格通常会做同样的事情,但它有遭受重载转换运算符的危险。

    【讨论】:

      【解决方案4】:

      C++ 有类型,它们通常相互转换的唯一方法是通过您编写的明确定义的转换运算符。一般来说,这就是您编写程序所需要和应该使用的全部内容。

      但是,有时您希望将表示类型的位重新解释为其他内容。这通常用于非常低级的操作,而不是您通常应该使用的东西。对于这些情况,您可以使用reinterpret_cast

      它是实现定义的,因为 C++ 标准并没有真正说明应该如何在内存中实际布局。这由您对 C++ 的特定实现控制。因此,reinterpret_cast 的行为取决于您的编译器如何在内存中布局结构以及它如何实现reinterpret_cast

      C 风格的强制转换与reinterpret_casts 非常相似,但它们的语法要少得多,因此不推荐使用。人们认为强制转换本质上是一个丑陋的操作,它需要丑陋的语法来通知程序员正在发生一些可疑的事情。

      一个可能出错的简单示例:

      std::string a;
      double* b;
      b = reinterpret_cast<double*>(&a);
      *b = 3.4;
      

      那个程序的行为是未定义的——编译器可以做任何它喜欢的事情。很可能,当string 的析构函数被调用时,你会崩溃,但谁知道呢!它可能只会损坏您的堆栈并导致不相关的函数崩溃。

      【讨论】:

      • 是最后一位实现还是未定义的行为(有一个重要区别)。
      • 未定义和实现定义有什么区别?
      • 1.3.10 实现定义的行为 行为,对于格式良好的程序构造和正确的数据,这取决于实现和每个实现文档。 1.3.24:未定义的行为本国际标准没有要求的行为。 1.3.25 未指定的行为 行为,对于格式良好的程序构造和正确的数据,取决于实现。在此处获取标准副本:stackoverflow.com/questions/81656/…
      • UB 是鼻恶魔。 ID 是针对这个特定的编译器/目标对的,其规范在手册中而不是标准中。
      【解决方案5】:

      reinterpret_cast 和 c 风格的转换都是实现定义的,它们做的事情几乎相同。区别在于:
      1.reinterpret_cast 无法移除常量。例如:

      const unsigned int d = 5;
      int *g=reinterpret_cast< int* >( &d );
      

      会报错:

      error: reinterpret_cast from type 'const unsigned int*' to type 'int*' casts away qualifiers  
      

      2。如果你使用reinterpret_cast,很容易找到你做的地方。不可能使用 c 风格的演员表

      【讨论】:

        【解决方案6】:

        C 风格的强制转换有时会以未指定的方式对对象进行类型双关语,例如 (unsigned int)-1,有时会将相同的值转换为不同的格式,例如 (double)42,有时也可以这样做,例如 (void*)0xDEADBEEF重新解释位,但(void*)0 保证为空指针常量,它不一定具有与(intptr_t)0 相同的对象表示,并且很少告诉编译器执行shoot_self_in_foot_with((char*)&amp;const_object); 之类的操作。

        这通常都很好,但是当您想将double 转换为uint64_t 时,有时您需要值,有时您需要位。如果您了解 C,您就知道 C 风格的转换是哪一种,但在某些方面为两者使用不同的语法会更好。

        Bjarne Stroustrup 在他的指导方针中,在另一种情况下推荐了 reinterpret_cast:如果你想以一种语言没有由 static_cast 定义的方式来键入双关语,他建议你使用类似 @ 987654331@ 而不是其他方法。它们都是未定义的行为,但这使得你在做什么以及你是故意这样做的非常明确。阅读与您上次写信不同的工会成员不会。

        【讨论】:

        • 什么是“双关语”?
        • @curiousguy 当你重新解释某个对象的二进制表示的位时,就好像它是不同的类型一样。例如,将 64 位 double 的位视为 uint64_t,以便您可以旋转它们。尤其是当方法声明union、写入一种类型的成员并读出不同类型的成员时(这在 C 中是合法的,但如您所知,在 C++ 中是未定义的行为)。
        • @curiousguy 某些整数转换可以。在有符号和无符号整数类型之间进行转换,从void*uintptr_t 的转换也是如此。另一方面,从doubleuint64_treinterpret_cast 是类型双关语,而C 风格的转换具有static_cast 的语义,尽可能接近地表示值。
        • @curiousguy [expr.reinterpret.cast.11],尽管这表示它需要转换为参考。
        • 是的,引用类型的reinterpret_cast 确实进行了重新解释。您之前的评论并不清楚使用了引用类型。
        猜你喜欢
        • 2019-03-25
        • 1970-01-01
        • 2012-08-14
        • 2011-04-04
        • 2014-11-29
        • 1970-01-01
        • 2011-06-20
        • 1970-01-01
        • 2011-05-27
        相关资源
        最近更新 更多