Quando usar reinterpret_cast?

459

Estou pouco confuso com a aplicabilidade do reinterpret_castvs static_cast. Pelo que li, as regras gerais são usar conversão estática quando os tipos podem ser interpretados em tempo de compilação, daí a palavra static. Esse é o elenco que o compilador C ++ usa internamente também para lançamentos implícitos.

reinterpret_casts são aplicáveis ​​em dois cenários:

  • converter tipos inteiros em tipos de ponteiros e vice-versa
  • converter um tipo de ponteiro para outro. A idéia geral que recebo é que isso não é portável e deve ser evitado.

Onde estou um pouco confuso é um uso que eu preciso, estou chamando C ++ de C e o código C precisa se apegar ao objeto C ++, então basicamente ele contém um void* . Que elenco deve ser usado para converter entre o void *tipo e o tipo de classe?

Eu já vi o uso de ambos static_caste reinterpret_cast? Embora pelo que tenho lido, parece que staticé melhor, pois o elenco pode acontecer em tempo de compilação? Embora diga para usar reinterpret_castpara converter de um tipo de ponteiro para outro?

HeretoLearn
fonte
9
reinterpret_castnão acontece no tempo de execução. Ambos são instruções em tempo de compilação. De en.cppreference.com/w/cpp/language/reinterpret_cast : "Diferentemente de static_cast, mas como const_cast, a expressão reinterpret_cast não compila com nenhuma instrução da CPU. É puramente uma diretiva de compilador que instrui o compilador a tratar a sequência de bits (representação de objeto) da expressão como se tivesse o tipo new_type. "
Cris Luengo
@ HeretoLearn, é possível adicionar partes de código relevantes dos arquivos * .c e * .cpp? Eu acho que isso pode melhorar a exposição da questão.
OrenIshShalom

Respostas:

442

O padrão C ++ garante o seguinte:

static_castum ponteiro de e para void*preserva o endereço. Ou seja, a seguir a,, be ctodos apontam para o mesmo endereço:

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

reinterpret_castgarante apenas que, se você converter um ponteiro para um tipo diferente e depois reinterpret_castretornar ao tipo original , obterá o valor original. Então, no seguinte:

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

ae ccontém o mesmo valor, mas o valor de bnão é especificado. (na prática, normalmente conterá o mesmo endereço que ae c, mas isso não está especificado no padrão e pode não ser verdade em máquinas com sistemas de memória mais complexos.)

Para fundição de e para void*, static_castdeve ser preferido.

jalf
fonte
18
Eu gosto do fato de que 'b' é indefinido. Ele o impede de fazer coisas tolas com ele. Se você converter algo para outro tipo de ponteiro, estará solicitando problemas e o fato de não poder depender disso o torna mais cuidadoso. Se você usou static_cast <> acima, de que serve o 'b'?
Martin Iorque
3
Eu pensei que reinterpret_cast <> garantisse o mesmo padrão de bits. (que não é o mesmo que um ponteiro válido para outro tipo).
Martin Iorque
37
o valor de bnão é mais não especificado no C ++ 11 ao usar reinterpret_cast. E em C ++ 03 um elenco de int*que void*foi proibido de ser feito com reinterpret_cast(embora compiladores não implementar isso e que era impraticável, portanto, foi mudado para C ++ 11).
Johannes Schaub - litb 28/10
55
Na verdade, isso não responde à pergunta "quando usar reinterpret_cast".
Einpoklum
6
@LokiAstari Eu acho que não especificado não o impede de fazer coisas tolas. Só o impede quando você se lembra de que não é especificado. Enorme diferença. Pessoalmente, não gosto de não especificado. Demais para lembrar.
Helin Wang
158

Um caso em que reinterpret_casté necessário é a interface com tipos de dados opacos. Isso ocorre frequentemente nas APIs do fornecedor sobre as quais o programador não tem controle. Aqui está um exemplo artificial em que um fornecedor fornece uma API para armazenar e recuperar dados globais arbitrários:

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

Para usar essa API, o programador deve converter seus dados para VendorGlobalUserDatae novamente. static_castnão vai funcionar, é preciso usar 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;
}

Abaixo está uma implementação artificial da API de exemplo:

// vendor.cpp
static VendorGlobalUserData g = 0;
void VendorSetUserData(VendorGlobalUserData p) { g = p; }
VendorGlobalUserData VendorGetUserData() { return g; }
jwfearn
fonte
7
Sim, esse é o único uso significativo de reinterpret_cast em que consigo pensar.
jalf
8
Essa pode ser uma pergunta tardia, mas por que a API do fornecedor não usa void*para isso?
Xeo
19
@ Xeo Eles não usam void * porque perdem (alguns) a verificação de tipo em tempo de compilação.
jesup
4
Um caso de uso prático de tipos de dados "opacos" é quando você deseja expor uma API ao C, mas escreve a implementação em C ++. A UTI é um exemplo de biblioteca que faz isso em vários locais. Por exemplo, na API do verificador de spoof, você lida com ponteiros do tipo USpoofChecker*, onde USpoofCheckeré uma estrutura vazia. No entanto, sob o capô, sempre que você passa um USpoofChecker*, ele sofre reinterpret_castum tipo interno de C ++.
Sffc 23/03/19
@ sffc porque não expor o tipo de estrutura C ao usuário?
Gupta
101

A resposta curta: se você não sabe o quereinterpret_cast significa, não use. Se você precisar no futuro, saberá.

Resposta completa:

Vamos considerar os tipos básicos de números.

Quando você converte, por exemplo, int(12)em unsigned float (12.0f)seu processador, precisa invocar alguns cálculos, pois os dois números têm uma representação de bits diferente. Isto é o que static_castsignifica.

Por outro lado, quando você chama reinterpret_casta CPU, não invoca nenhum cálculo. Apenas trata um conjunto de bits na memória como se tivesse outro tipo. Portanto, quando você converte int*para float*com essa palavra-chave, o novo valor (após a redução da referência do ponteiro) não tem nada a ver com o valor antigo em significado matemático.

Exemplo: É verdade quereinterpret_castnão é portátil devido a uma ordem de razão de bytes (endianness). Mas isso é surpreendentemente o melhor motivo para usá-lo. Vamos imaginar o exemplo: você precisa ler o número binário de 32 bits do arquivo e sabe que é big endian. Seu código precisa ser genérico e funciona corretamente em sistemas big endian (por exemplo, alguns ARM) e little endian (por exemplo, x86). Então você tem que verificar a ordem dos bytes. É bem conhecido no tempo de compilação para que você possa escrever a constexprfunção: Você pode escrever uma função para conseguir isso:

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

Explicação: a representação binária dexna memória pode ser0000'0000'0000'0001(grande) ou0000'0001'0000'0000(pouco endian). Após reinterpretar a conversão, o byte sob opponteiro pode ser respectivamente0000'0000ou0000'0001. Se você usar projeção estática, sempre será0000'0001, independentemente de qual endianness esteja sendo usado.

EDITAR:

Na primeira versão que fiz exemplo função is_little_endiande ser constexpr. Ele compila bem no mais recente gcc (8.3.0), mas o padrão diz que é ilegal. O compilador clang se recusa a compilá-lo (o que está correto).

jaskmar
fonte
1
Belo exemplo! Substituiria abreviação de uint16_t e char não assinado por uint8_t para torná-lo menos obscuro para humanos.
Jan Turon
@ JanTuroň verdade, não podemos assumir que shortleva 16 bits na memória. Corrigido.
jaskmar
1
O exemplo está errado. reinterpret_cast não é permitido em funções constexpr
Michael Veksler
1
Primeiro de tudo, esse código é rejeitado pelo clang mais recente (7.0.0) e pelo gcc (8.2.0). Infelizmente, não encontrei a limitação na linguagem formal. Tudo o que eu consegui encontrar foi social.msdn.microsoft.com/Forums/vstudio/pt-BR/
Michael Veksler
2
Mais especificamente, en.cppreference.com/w/cpp/language/constant_expression (item 16) afirma claramente que reinterpret_cast não pode ser usado em uma expressão constante. Veja também github.com/cplusplus/draft/blob/master/papers/N3797.pdf (5.19 expressões constantes) páginas125-126, que exclui explicitamente reinterpret_cast. Então 7.1.5 O constexpr especificador o item 5 (página 146) * Para um não-modelo, função constexpr não inadimplente ... se há valores de argumento existir tal que ... poderia ser uma sub-expressão avaliada de uma expressão essencial constante (5,19 ), o programa está mal formado *
Michael Veksler
20

O significado de reinterpret_castnão é definido pelo padrão C ++. Portanto, em teoria, um reinterpret_castpoderia travar seu programa. Na prática, os compiladores tentam fazer o que você espera, que é interpretar os bits do que você está passando como se fossem do tipo para o qual você está transmitindo. Se você sabe o que os compiladores que você vai usar fazem, reinterpret_cast pode usá-lo, mas dizer que é portátil estaria mentindo.

Para o caso que você descreve, e praticamente qualquer caso em que possa considerar reinterpret_cast, você pode usar static_castou alguma outra alternativa. Entre outras coisas, o padrão tem a dizer sobre o que você pode esperar static_cast(§5.2.9):

Um rvalor do tipo “ponteiro para cv void” pode ser explicitamente convertido em um ponteiro para o tipo de objeto. Um valor do tipo ponteiro para o objeto convertido em “ponteiro para cv void” e de volta ao tipo de ponteiro original terá seu valor original.

Portanto, para o seu caso de uso, parece bastante claro que o comitê de padronização pretendia que você usasse static_cast.

flodin
fonte
5
Não basta travar seu programa. O padrão oferece algumas garantias sobre reinterpret_cast. Apenas não quantas pessoas as pessoas esperam.
jalf
1
Não se você usá-lo corretamente. Ou seja, reinterpretar_cast de A a B para A é perfeitamente seguro e bem definido. Mas o valor de B não é especificado e, sim, se você confiar nisso, coisas ruins podem acontecer. Mas o elenco em si é seguro o suficiente, desde que você o use apenas da maneira que o padrão permitir. ;)
jalf
55
lol, suspeito que reinterpret_crash possa realmente travar seu programa. Mas reinterpret_cast não. ;)
jalf
5
<irony> Eu tentei no meu compilador e, de alguma forma, ele se recusou a compilar reinterpret_crash. De maneira alguma um bug do compilador me impedirá de travar meu programa de reinterpretação. Vou relatar um erro o mais rápido possível </ ironia>!
paercebal
18
@paercebaltemplate<class T, U> T reinterpret_crash(U a) { return *(T*)nullptr; }
12

Um uso do reinterpret_cast é se você deseja aplicar operações bit a bit para flutuadores (IEEE 754). Um exemplo disso foi o truque Fast Inverse Square-Root:

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

Ele trata a representação binária do flutuador como um número inteiro, o desloca para a direita e o subtrai de uma constante, diminuindo pela metade e negando o expoente. Depois de converter novamente em um flutuador, ele é submetido a uma iteração de Newton-Raphson para tornar essa aproximação mais exata:

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;
}

Isso foi originalmente escrito em C, então usa conversão de C, mas a conversão de C ++ análoga é a reinterpret_cast.

Adam P. Goucher
fonte
1
error: invalid cast of an rvalue expression of type 'int64_t {aka long long int}' to type 'double&' reinterpret_cast<double&>((reinterpret_cast<int64_t&>(d) >> 1) + (1L << 61))# ideone.com/6S4ijc
Orwellophile
1
O padrão diz que este é um comportamento indefinido: en.cppreference.com/w/cpp/language/reinterpret_cast (em "aliasing de tipo")
Cris Luengo
@CrisLuengo Se eu substituir todos reinterpret_castpor memcpy, ainda é UB?
Sandthorn 16/07/19
@ Sandthorn: Este é o UB de acordo com o padrão, mas se funcionar para sua arquitetura, não se preocupe. Esse truque é bom, presumo, para qualquer compilador para arquiteturas Intel. Não funcionou como planejado (ou até travou) em outras arquiteturas - por exemplo, é possível que carros alegóricos e compridos sejam armazenados em compartimentos de memória separados (não que eu conheça essa arquitetura, é apenas um argumento ...) . memcpydefinitivamente tornaria legal.
Cris Luengo
2
template <class outType, class inType>
outType safe_cast(inType pointer)
{
    void* temp = static_cast<void*>(pointer);
    return static_cast<outType>(temp);
}

Tentei concluir e escrevi um elenco seguro simples usando modelos. Observe que esta solução não garante converter ponteiros em uma função.

Sasha Zezulinsky
fonte
1
O que? Porque se importar? É exatamente isso que reinterpret_castjá faz nesta situação: "Um ponteiro de objeto pode ser explicitamente convertido em um ponteiro de objeto de um tipo diferente. [72] Quando um valor inicial v do tipo de ponteiro de objeto é convertido no tipo de ponteiro de objeto" ponteiro para cv T ", o resultado é static_cast<cv T*>(static_cast<cv void*>(v))". - N3797.
underscore_d
Quanto c++2003padrão I pode não achar que reinterpret_castfazstatic_cast<cv T*>(static_cast<cv void*>(v))
Sasha Zezulinsky
1
OK, é verdade, mas não me importo com uma versão de 13 anos atrás, e a maioria dos programadores também não deve se (como é provável) eles podem evitá-la. Respostas e comentários devem realmente refletir o último padrão disponível, a menos que especificado de outra forma ... IMHO. De qualquer forma, acho que o Comitê sentiu a necessidade de adicionar isso explicitamente depois de 2003. (porque o IIRC era o mesmo no C ++ 11) #
underscore_d
Antes C++03era C++98. Toneladas de projetos usavam C ++ antigo em vez de C. portátil. Às vezes, você precisa se preocupar com a portabilidade. Por exemplo, você precisa suportar o mesmo código no Solaris, AIX, HPUX, Windows. No que diz respeito à dependência e portabilidade do compilador, é complicado. Assim, um bom exemplo da introdução de um inferno portabilidade é usar um reinterpret_castno seu código
Sasha Zezulinsky
novamente, se como eu, você ficará feliz em se limitar apenas às plataformas que se adaptam bem à versão mais recente e melhor da linguagem, sua objeção é um ponto discutível.
underscore_d
1

Primeiro você tem alguns dados em um tipo específico como int aqui:

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

Então você deseja acessar a mesma variável que outro tipo, como float: Você pode decidir entre

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

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

ou

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

//this could be used in c and cpp

BREVE: significa que a mesma memória é usada como um tipo diferente. Assim, você pode converter representações binárias de carros alegóricos como tipo int, como acima, em carros alegóricos. 0x80000000 é -0, por exemplo (a mantissa e o expoente são nulos, mas o sinal, o msb, é um. Isso também funciona para duplos e duplos longos.

OTIMIZE: Acho que reinterpret_cast seria otimizado em muitos compiladores, enquanto a conversão c é feita por pointeraritmética (o valor deve ser copiado para a memória, porque os ponteiros não podem apontar para os registradores de CPU).

NOTA: Nos dois casos, você deve salvar o valor convertido em uma variável antes da conversão! Essa macro pode ajudar:

#define asvar(x) ({decltype(x) __tmp__ = (x); __tmp__; })
cmdLP
fonte
É verdade que "significa que a mesma memória é usada como um tipo diferente", mas está restrita a um par de tipos específico. No seu exemplo reinterpret_castforma inta float&é um comportamento indefinido.
jaskmar
1

Uma razão para usar reinterpret_casté quando uma classe base não possui uma tabela v, mas uma classe derivada possui. Nesse caso, static_caste reinterpret_castresultará em diferentes valores de ponteiro (este seria o caso atípico mencionado por jalf acima ). Apenas como um aviso, não estou afirmando que isso faz parte do padrão, mas a implementação de vários compiladores generalizados.

Como exemplo, pegue o código abaixo:

#include <cstdio>

class A {
public:
    int i;
};

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

int main()
{
    B b;
    const A* a = static_cast<A*>(&b);
    const A* ar = reinterpret_cast<A*>(&b);

    printf("&b = %p\n", &b);
    printf(" a = %p\n", a);
    printf("ar = %p\n", ar);
    printf("difference = %ld\n", (long int)(a - ar));

    return 0;
}

Que gera algo como:

& b = 0x7ffe10e68b38
a = 0x7ffe10e68b40
ar = 0x7ffe10e68b38
diferença = 2

Em todos os compiladores que tentei (MSVC 2015 e 2017, clang 8.0.0, gcc 9.2, icc 19.0.1 - veja godbolt nos últimos 3 ), o resultado da diferençastatic_cast é de reinterpret_cast2 (4 para MSVC). O único compilador a alertar sobre a diferença foi clang, com:

17:16: aviso: 'reinterpret_cast' da classe 'B *' para sua base com deslocamento diferente de zero 'A *' se comporta de maneira diferente de 'static_cast' [-Wreinterpret-base-class]
const A * ar = reinterpret_cast (& b) ;
^ ~~~~~~~~~~~~~~~~~~~~~~
17:16: note: use 'static_cast' para ajustar o ponteiro corretamente enquanto faz upcasting
const A * ar = reinterpret_cast (& b) ;
^ ~~~~~~~~~~~~~~~
static_cast

Uma última ressalva é que, se a classe base não tiver membros de dados (por exemplo, the int i;), clang, gcc e icc retornam o mesmo endereço para o reinterpret_castque para static_cast, enquanto o MSVC ainda não.

Avi Ginsburg
fonte
1

Aqui está uma variante do programa de Avi Ginsburg que ilustra claramente a propriedade reinterpret_castmencionada por Chris Luengo, flodin e cmdLP: que o compilador trata o local da memória apontada como se fosse um objeto do novo tipo:

#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);
}

O que resulta em resultados como este:

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.

Pode-se observar que o objeto B é construído na memória como dados específicos de B primeiro, seguido pelo objeto A incorporado. O static_castretorna corretamente o endereço do objeto Um incorporado, eo ponteiro criado por static_castcorretamente fornece o valor do campo de dados. O ponteiro gerado por reinterpret_castguloseimasb localização da memória de como se fosse um objeto A simples; portanto, quando o ponteiro tenta obter o campo de dados, ele retorna alguns dados específicos de B como se fossem o conteúdo desse campo.

Um uso de reinterpret_casté converter um ponteiro em um número inteiro não assinado (quando ponteiros e números inteiros não assinados têm o mesmo tamanho):

int i; unsigned int u = reinterpret_cast<unsigned int>(&i);

TRPh
fonte
-6

Resposta rápida: use static_castse compilar, caso contrário, recorra a reinterpret_cast.

Marius K
fonte
-16

Leia o FAQ ! Manter dados C ++ em C pode ser arriscado.

No C ++, um ponteiro para um objeto pode ser convertido void *sem nenhuma conversão . Mas não é verdade o contrário. Você precisaria de um static_castpara recuperar o ponteiro original.

dirkgently
fonte