以下各项的正确用途是什么:

静态铸造动态铸造常量_成本重新解释(_C)(type)值(C样式转换)类型(值)(函数样式转换)

如何决定在哪些特定情况下使用哪一种?


当前回答

下播/上播上的staticcast vs dynamiccast vs reinterpret_cast内部视图

在这个答案中,我想在一个具体的上/下转换示例中比较这三种机制,并分析底层指针/内存/程序集的情况,以具体了解它们的比较方式。

我相信,这将给我们一个很好的直觉,说明这些演员的不同之处:

staticcast:在运行时执行一个地址偏移(低运行时影响),并且没有安全检查下变频是否正确。dyanamic_cast:在运行时执行与static_cast相同的地址偏移,但也使用RTTI进行昂贵的安全检查,以确保下变频正确。此安全检查允许您在运行时通过检查nullptr的返回来查询基类指针是否为给定类型,该返回指示无效的下变频。因此,如果您的代码无法检查该nullptr并采取有效的非中止操作,那么应该只使用static_cast而不是动态强制转换。如果中止是代码可以执行的唯一操作,那么您可能只想在调试版本(-NEDBUG)中启用dynamic_cast,否则使用static_cast,例如,如这里所做的,以不减慢快速运行。reinterprecast:在运行时不执行任何操作,甚至不执行地址偏移。指针必须精确指向正确的类型,即使基类也无法工作。除非涉及原始字节流,否则通常不需要这样做。

考虑以下代码示例:

主.cpp

#include <iostream>

struct B1 {
    B1(int int_in_b1) : int_in_b1(int_in_b1) {}
    virtual ~B1() {}
    void f0() {}
    virtual int f1() { return 1; }
    int int_in_b1;
};

struct B2 {
    B2(int int_in_b2) : int_in_b2(int_in_b2) {}
    virtual ~B2() {}
    virtual int f2() { return 2; }
    int int_in_b2;
};

struct D : public B1, public B2 {
    D(int int_in_b1, int int_in_b2, int int_in_d)
        : B1(int_in_b1), B2(int_in_b2), int_in_d(int_in_d) {}
    void d() {}
    int f2() { return 3; }
    int int_in_d;
};

int main() {
    B2 *b2s[2];
    B2 b2{11};
    D *dp;
    D d{1, 2, 3};

    // The memory layout must support the virtual method call use case.
    b2s[0] = &b2;
    // An upcast is an implicit static_cast<>().
    b2s[1] = &d;
    std::cout << "&d           " << &d           << std::endl;
    std::cout << "b2s[0]       " << b2s[0]       << std::endl;
    std::cout << "b2s[1]       " << b2s[1]       << std::endl;
    std::cout << "b2s[0]->f2() " << b2s[0]->f2() << std::endl;
    std::cout << "b2s[1]->f2() " << b2s[1]->f2() << std::endl;

    // Now for some downcasts.

    // Cannot be done implicitly
    // error: invalid conversion from ‘B2*’ to ‘D*’ [-fpermissive]
    // dp = (b2s[0]);

    // Undefined behaviour to an unrelated memory address because this is a B2, not D.
    dp = static_cast<D*>(b2s[0]);
    std::cout << "static_cast<D*>(b2s[0])            " << dp           << std::endl;
    std::cout << "static_cast<D*>(b2s[0])->int_in_d  " << dp->int_in_d << std::endl;

    // OK
    dp = static_cast<D*>(b2s[1]);
    std::cout << "static_cast<D*>(b2s[1])            " << dp           << std::endl;
    std::cout << "static_cast<D*>(b2s[1])->int_in_d  " << dp->int_in_d << std::endl;

    // Segfault because dp is nullptr.
    dp = dynamic_cast<D*>(b2s[0]);
    std::cout << "dynamic_cast<D*>(b2s[0])           " << dp           << std::endl;
    //std::cout << "dynamic_cast<D*>(b2s[0])->int_in_d " << dp->int_in_d << std::endl;

    // OK
    dp = dynamic_cast<D*>(b2s[1]);
    std::cout << "dynamic_cast<D*>(b2s[1])           " << dp           << std::endl;
    std::cout << "dynamic_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;

    // Undefined behaviour to an unrelated memory address because this
    // did not calculate the offset to get from B2* to D*.
    dp = reinterpret_cast<D*>(b2s[1]);
    std::cout << "reinterpret_cast<D*>(b2s[1])           " << dp           << std::endl;
    std::cout << "reinterpret_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
}

编译、运行和反汇编:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
setarch `uname -m` -R ./main.out
gdb -batch -ex "disassemble/rs main" main.out

其中setarch用于禁用ASLR,以便于比较跑步。

可能的输出:

&d           0x7fffffffc930
b2s[0]       0x7fffffffc920
b2s[1]       0x7fffffffc940
b2s[0]->f2() 2
b2s[1]->f2() 3
static_cast<D*>(b2s[0])            0x7fffffffc910
static_cast<D*>(b2s[0])->int_in_d  1
static_cast<D*>(b2s[1])            0x7fffffffc930
static_cast<D*>(b2s[1])->int_in_d  3
dynamic_cast<D*>(b2s[0])           0
dynamic_cast<D*>(b2s[1])           0x7fffffffc930
dynamic_cast<D*>(b2s[1])->int_in_d 3
reinterpret_cast<D*>(b2s[1])           0x7fffffffc940
reinterpret_cast<D*>(b2s[1])->int_in_d 32767

现在,如以下所述:https://en.wikipedia.org/wiki/Virtual_method_table为了有效地支持虚拟方法调用,假设B1的存储器数据结构具有以下形式:

B1:
  +0: pointer to virtual method table of B1
  +4: value of int_in_b1

B2的形式为:

B2:
  +0: pointer to virtual method table of B2
  +4: value of int_in_b2

那么D的存储器数据结构必须看起来像:

D:
  +0: pointer to virtual method table of D (for B1)
  +4: value of int_in_b1
  +8: pointer to virtual method table of D (for B2)
 +12: value of int_in_b2
 +16: value of int_in_d

关键事实是D的存储器数据结构内部包含与B1和B2相同的存储器结构,即:

+0看起来很像B1,D的B1 vtable后跟int_in_B1+8看起来很像B2,D的B2 vtable后跟int_in_B2

或更高级别:

D:
   +0: B1
   +8: B2
  +16: <fields of D itsef>

因此,我们得出关键结论:

上变频或下变频只需要将指针值移位一个编译时已知的值

这样,当D被传递到基类型数组时,类型转换实际上计算了该偏移量,并指向了内存中看起来与有效B2完全相同的对象,只是这个对象具有D的vtable而不是B2,因此所有虚拟调用都是透明的。

例如。:

b2s[1] = &d;

简单地需要获得d+8的地址以到达对应的B2类数据结构。

现在,我们终于可以回到类型铸造和具体示例的分析。

从标准输出中,我们可以看到:

&d           0x7fffffffc930
b2s[1]       0x7fffffffc940

因此,在那里完成的隐式static_cast确实正确地计算了从0x7fffffffc930处的全D数据结构到0x7ffFFfc940处的B2类数据结构的偏移。我们还推断,位于0x7fffffffc930和0x7ffFFfc940之间的可能是B1数据和vtable。

然后,在下降部分,现在很容易理解无效部分是如何失败的以及原因:

static_cast<D*>(b2s[0])0x7fffffffc910:编译器在编译时字节数增加0x10,尝试从B2转到包含D但由于b2s[0]不是D,它现在指向一个未定义的内存区域。拆卸是:49 dp=静态铸造<D*>(b2s[0]);0x0000000000000fc8<+414>:48 8b 45 d0 mov-0x30(%rbp),%rax0x0000000000000fcc<+418>:48 85 c0测试%rax,%rax0x0000000000000fcf<+421>:74 0a je 0xfdb<main()+433>0x0000000000000fd1<+423>:48 8b 45 d0 mov-0x30(%rbp),%rax0x0000000000000fd5<+427>:48 83 e8 10 sub$0x10,%rax0x0000000000000fd9<+431>:eb 05 jmp 0xfe0<main()+438>0x0000000000000fdb<+433>:b8 00 00 00 mov$0x0,%eax0x0000000000000fe0<+438>:48 89 45 98 mov%rax,-0x68(%rbp)因此我们看到GCC确实:检查指针是否为NULL,如果为,则返回NULL否则,从中减去0x10以达到不存在的Ddynamic_cast<D*>(b2s[0])0:C++实际上发现该转换无效,并返回nullptr!编译时无法做到这一点,我们将从反汇编中确认:59 dp=动态铸造<D*>(b2s[0]);0x00000000000010ec<+706>:48 8b 45 d0 mov-0x30(%rbp),%rax0x00000000000010f0<+710>:48 85 c0测试%rax,%rax0x00000000000010f3<+713>:74 1d je 0x1112<main()+744>0x00000000000010f5<+715>:b9 10 00 00 mov$0x10,%ecx0x00000000000010fa<+720>:48 8d 15 f7 0b 20 00 lea 0x200bf7(%rip),%rdx#0x201cf8<_ZTI1D>0x0000000000001101<+727>:48 8d 35 28 0c 20 00 lea 0x200c28(%rip),%rsi#0x201d30<_ZTI2B2>0x0000000000001108<+734>:48 89 c7 mov%rax,%rdi0x000000000000110b<+737>:e8 c0 fb ff ff callq 0xcd0<__dynamic_cast@plt>0x0000000000001110<+742>:eb 05 jmp 0x1117<main()+749>0x0000000000001112<+744>:b8 00 00 00 mov$0x0,%eax0x0000000000001117<+749>:48 89 45 98 mov%rax,-0x68(%rbp)首先有一个NULL检查,如果输入为NULL,则返回NULL。否则,它会在RDX、RSI和RDI中设置一些参数,并调用__dynamic_cast。我现在没有耐心进一步分析这个问题,但正如其他人所说,唯一可行的方法是__dynamic_cast访问内存中表示类层次结构的一些额外RTTI数据结构。因此,它必须从该表的B2条目开始,然后遍历该类层次结构,直到找到b2s[0]中的D类型转换的vtable。这就是为什么动态铸造可能很昂贵!这里有一个例子,在一个复杂的项目中,一个将dynamic_cast转换为static_cast的单线补丁将运行时间减少了33%!。reinterpret_cast<D*>(b2s[1])0x7fffffffc940这个只是盲目地相信我们:我们说地址b2s[1]处有一个D,编译器不进行偏移计算。但这是错误的,因为D实际上位于0x7fffffffc930,而位于0x7fffffffc940的是D内部的类似B2的结构!这样垃圾就可以进入了。我们可以从可怕的-O0组件中确认这一点,该组件只会移动值:70 dp=重新解释成本<D*>(b2s[1]);0x00000000000011fa<+976>:48 8b 45 d8 mov-0x28(%rbp),%rax0x00000000000011fe<+980>:48 89 45 98 mov%rax,-0x68(%rbp)

相关问题:

何时应使用static_cast、dynamic_cast、const_cast和reinterpret_cast?dynamic_cast是如何实现的使用C中的“static_cast”进行降频++

在Ubuntu 18.04 amd64、GCC 7.4.0上测试。

其他回答

使用dynamic_cast转换继承层次结构中的指针/引用。对普通类型转换使用static_cast。使用reinterpret_cast对位模式进行低级重新解释。使用时要格外小心。使用const_cast丢弃const/vatile。避免这种情况,除非您使用的是常量错误的API。

虽然其他答案很好地描述了C++转换之间的所有差异,但我想补充一点,为什么不应该使用C样式转换(Type)var和Type(var)。

对于C++初学者来说,C风格的强制转换看起来像是C++强制转换的超集操作(static_cast<>()、dynamic_cast<<()、const_cast<>()、reinterpret_cast<>(。事实上,C样式转换是超集,编写起来更短。

C风格转换的主要问题是它们隐藏了开发人员转换的真实意图。C样式强制转换几乎可以执行所有类型的强制转换,从static_cast<>()和dynamic_cast<>()执行的通常安全的强制转换到const_cast<()等潜在的危险强制转换,其中可以删除const修饰符,以便可以修改const变量并重新解释cast<>(,甚至可以将整数值重新解释为指针。

这是样品。

int a=rand(); // Random number.

int* pa1=reinterpret_cast<int*>(a); // OK. Here developer clearly expressed he wanted to do this potentially dangerous operation.

int* pa2=static_cast<int*>(a); // Compiler error.
int* pa3=dynamic_cast<int*>(a); // Compiler error.

int* pa4=(int*) a; // OK. C-style cast can do such cast. The question is if it was intentional or developer just did some typo.

*pa4=5; // Program crashes.

C++转换被添加到语言中的主要原因是为了让开发人员明确自己的意图——为什么要进行转换。通过使用在C++中完全有效的C样式转换,您的代码可读性降低,更容易出错,尤其是对于其他没有创建代码的开发人员。因此,为了使代码更加可读和明确,您应该始终倾向于C++转换而不是C样式转换。

这是Bjarne Stroustrup(C++的作者)的书《C++编程语言》第四版第302页的一段简短引用。

这种C样式转换比命名的转换运算符更危险因为在大型程序中很难发现这种符号,而且程序员想要进行的转换也不明确。

reinterpret_cast的好特性(其他答案中没有提到)是它允许我们为函数类型创建一种void*指针。通常,对于对象类型,使用static_cast检索存储在void*中的指针的原始类型:

  int i = 13;
  void *p = &i;
  auto *pi = static_cast<int*>(p);

对于函数,我们必须使用reinterpret_cast两次:

#include<iostream>

using any_fcn_ptr_t = void(*)();


void print(int i)
{
   std::cout << i <<std::endl;
}

int main()
{     
  //Create type-erased pointer to function:
  auto any_ptr = reinterpret_cast<any_fcn_ptr_t>(&print);
  
  //Retrieve the original pointer:
  auto ptr = reinterpret_cast< void(*)(int) >(any_ptr);
  
  ptr(7);
}

使用reinterpret_cast,我们甚至可以为指向成员函数的指针获得类似类型的void*指针。

与普通的void*和static_cast一样,C++保证ptr指向print函数(只要我们将正确的类型传递给reinterpret_cast)。

staticcast是您应该尝试使用的第一个强制转换。它执行类型之间的隐式转换(例如int到float,或指向void*的指针),还可以调用显式转换函数(或隐式转换函数)。在许多情况下,显式声明static_cast是不必要的,但需要注意的是,t(something)语法相当于(t)something,应该避免(稍后将详细介绍)。然而,T(something,something_else)是安全的,并保证调用构造函数。

staticcast还可以通过继承层次结构进行转换。当向上转换(向基类)时,这是不必要的,但当向下转换时,只要不通过虚拟继承进行转换,就可以使用它。但是,它不进行检查,并且将层次结构静态转换为实际上不是对象类型的类型是未定义的行为。


const_cast可用于移除或添加常量到变量;没有其他C++转换能够移除它(甚至没有重新解释cast)。需要注意的是,只有当原始变量为常量时,修改以前的常量值才是未定义的;如果使用它来删除对未使用const声明的对象的引用的const,那么它是安全的。例如,当基于常量重载成员函数时,这很有用。它还可以用于向对象添加常量,例如调用成员函数重载。

constcast在volatile上也有类似的作用,尽管这不太常见。


dynamiccast专门用于处理多态性。您可以将指向任何多态类型的指针或引用强制转换为任何其他类类型(多态类型至少有一个声明或继承的虚拟函数)。你可以使用它不仅仅是向下投掷——你可以向侧面投掷,甚至向上投掷另一个链条。dynamic_cast将查找所需的对象,并在可能的情况下将其返回。如果不能,它将在指针的情况下返回nullptr,或者在引用的情况下抛出std::bad_cast。

不过dynamiccast有一些限制。如果继承层次结构中有多个相同类型的对象(所谓的“可怕的钻石”),并且您没有使用虚拟继承,那么它就不起作用。它也只能通过公共继承——它总是无法通过受保护或私人继承。然而,这很少是一个问题,因为这种形式的继承是罕见的。


reinterprecast是最危险的演员,应该非常谨慎地使用。它将一种类型直接转换为另一种类型,例如将值从一个指针转换为另另一个指针,或将指针存储在int中,或其他各种讨厌的事情。在很大程度上,使用reinterpret_cast可以得到的唯一保证是,通常情况下,如果将结果强制转换回原始类型,将得到完全相同的值(但如果中间类型小于原始类型,则不会)。还有许多转换是reinterpret_cast无法完成的。它主要用于特别奇怪的转换和位操作,例如将原始数据流转换为实际数据,或将数据存储在指向对齐数据的指针的低位。


C样式转换和函数样式转换分别是使用(类型)对象或类型(对象)的转换,在功能上是等效的。它们被定义为以下成功的第一个:

常量_成本static_cast(尽管忽略访问限制)static_cast(见上文),然后是const_cast重新解释(_C)重新解释成本,然后常量成本

因此,在某些情况下,它可以用作其他强制转换的替代品,但可能会非常危险,因为它可以转换为重新解释强制转换,当需要显式强制转换时,应首选后者,除非您确定static_cast将成功或重新解释强制。即使如此,也要考虑更长、更明确的选项。

C样式转换在执行static_cast时也会忽略访问控制,这意味着它们能够执行其他转换无法执行的操作。不过,这基本上是一个拙劣的说法,在我看来,这只是避免C型造型的另一个原因。

除了到目前为止的其他答案之外,这里有一个不明显的例子,其中static_cast不够,因此需要重新解释cast。假设有一个函数在输出参数中返回指向不同类(不共享公共基类)对象的指针。此类函数的一个真实示例是CoCreateInstance()(请参阅最后一个参数,实际上是void**)。假设您从这个函数中请求特定的对象类,这样您就可以提前知道指针的类型(这通常是针对COM对象的)。在这种情况下,不能使用static_cast将指向指针的指针转换为void**:您需要重新解释cast<void**>(&yourPointer)。

在代码中:

#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
    CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
    //static_cast<void**>(&pNetFwPolicy2) would give a compile error
    reinterpret_cast<void**>(&pNetFwPolicy2) );

但是,static_cast适用于简单指针(而不是指向指针的指针),因此可以通过以下方式重写上述代码以避免重新解释cast(代价是额外的变量):

#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
void* tmp = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
    CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
    &tmp );
pNetFwPolicy2 = static_cast<INetFwPolicy2*>(tmp);