Quand utiliser reinterpret_cast?

460

Je suis un peu confus avec l'applicabilité de reinterpret_castvs static_cast. D'après ce que j'ai lu, les règles générales consistent à utiliser une conversion statique lorsque les types peuvent être interprétés au moment de la compilation, d'où le mot static. C'est le cast que le compilateur C ++ utilise également en interne pour les conversions implicites.

reinterpret_casts sont applicables dans deux scénarios:

  • convertir des types entiers en types pointeurs et vice versa
  • convertir un type de pointeur en un autre. L'idée générale que je me fais est que ce n'est pas transférable et doit être évité.

Là où je suis un peu confus, c'est une utilisation dont j'ai besoin, j'appelle C ++ à partir de C et le code C doit s'accrocher à l'objet C ++, donc en gros il contient un void*. Quelle conversion doit être utilisée pour convertir entre le type void *et le type Class?

J'ai vu l'utilisation des deux static_castet reinterpret_cast? Bien que d'après ce que j'ai lu, il semble staticque la diffusion puisse être meilleure au moment de la compilation? Bien qu'il indique d'utiliser reinterpret_castpour convertir d'un type de pointeur à un autre?

HeretoLearn
la source
9
reinterpret_castne se produit pas au moment de l'exécution. Ce sont deux instructions de compilation. De en.cppreference.com/w/cpp/language/reinterpret_cast : "Contrairement à static_cast, mais comme const_cast, l'expression reinterpret_cast ne se compile dans aucune instruction CPU. Il s'agit purement d'une directive de compilation qui demande au compilateur de traiter la séquence de bits (représentation d'objet) d'expression comme si elle avait le type new_type. "
Cris Luengo
@HeretoLearn, est-il possible d'ajouter les morceaux de code appropriés à partir des fichiers * .c et * .cpp? Je pense que cela peut améliorer l'exposé de la question.
OrenIshShalom

Réponses:

443

La norme C ++ garantit les éléments suivants:

static_castun pointeur vers et depuis void*conserve l’adresse. C'est-à-dire, dans ce qui suit,a , bet ctous les points à la même adresse:

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

reinterpret_cast garantit uniquement que si vous transformez un pointeur en un autre type, puis en reinterpret_castle replaçant dans le type d'origine , vous obtenez la valeur d'origine. Donc, dans ce qui suit:

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

aet ccontiennent la même valeur, mais la valeur de bn'est pas spécifiée. (dans la pratique, il contiendra généralement la même adresse que aet c, mais ce n'est pas spécifié dans la norme, et cela peut ne pas être vrai sur les machines avec des systèmes de mémoire plus complexes.)

Pour la coulée vers et depuis void*, static_castdoit être préférée.

jalf
la source
18
J'aime le fait que «b» ne soit pas défini. Cela vous empêche de faire des bêtises avec. Si vous transformez quelque chose en un autre type de pointeur, vous posez des problèmes et le fait que vous ne puissiez pas en dépendre vous rend plus prudent. Si vous aviez utilisé static_cast <> ci-dessus à quoi sert le 'b' de toute façon?
Martin York
3
Je pensais que reinterpret_cast <> garantissait le même motif binaire. (qui n'est pas identique à un pointeur valide vers un autre type).
Martin York
37
la valeur de bn'est plus non spécifiée en C ++ 11 lors de l'utilisation reinterpret_cast. Et en C ++ 03, il était interdit de faire un cast de int*to (bien que les compilateurs ne l'aient pas implémenté et que cela ne soit pas pratique, il a donc été modifié pour C ++ 11). void*reinterpret_cast
Johannes Schaub - litb
55
Cela ne répond pas réellement à la question "quand utiliser reinterpret_cast".
einpoklum
6
@LokiAstari Je pense que le fait de ne pas préciser ne vous empêche pas de faire des bêtises. Cela ne vous arrête que lorsque vous vous souvenez que cela n'est pas spécifié. Énorme différence. Personnellement, je n'aime pas non spécifié. Trop de choses à retenir.
Helin Wang
158

Un cas où cela reinterpret_castest nécessaire est lors de l'interfaçage avec des types de données opaques. Cela se produit fréquemment dans les API des fournisseurs sur lesquelles le programmeur n'a aucun contrôle. Voici un exemple artificiel où un fournisseur fournit une API pour stocker et récupérer des données globales arbitraires:

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

Pour utiliser cette API, le programmeur doit convertir et inverser ses données VendorGlobalUserData. static_castne fonctionnera pas, il faut utiliser 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;
}

Voici une implémentation artificielle de l'exemple d'API:

// vendor.cpp
static VendorGlobalUserData g = 0;
void VendorSetUserData(VendorGlobalUserData p) { g = p; }
VendorGlobalUserData VendorGetUserData() { return g; }
jwfearn
la source
7
Oui, c'est à peu près la seule utilisation significative de reinterpret_cast à laquelle je peux penser.
jalf
8
Cela peut être une question tardive, mais pourquoi l'API du fournisseur ne l'utilise void*-t-elle pas?
Xeo
19
@Xeo Ils n'utilisent pas void * car ils perdent (certains) la vérification de type lors de la compilation.
jesup
4
Un cas d'utilisation pratique des types de données "opaques" consiste à exposer une API à C mais à écrire l'implémentation en C ++. ICU est un exemple de bibliothèque qui fait cela à plusieurs endroits. Par exemple, dans l'API Spoof Checker, vous traitez des pointeurs de type USpoofChecker*, où USpoofCheckerest une structure vide. Cependant, sous le capot, chaque fois que vous passez un USpoofChecker*, il subit reinterpret_castun type C ++ interne.
sffc
@sffc pourquoi ne pas exposer le type de structure C à l'utilisateur?
Gupta
101

La réponse courte: si vous ne savez pas ce que reinterpret_castsignifie, ne l'utilisez pas. Si vous en aurez besoin à l'avenir, vous le saurez.

Réponse complète:

Prenons les types de nombres de base.

Lorsque vous convertissez par exemple int(12)à unsigned float (12.0f)vos besoins de processeur pour appeler certains calculs que les deux nombres a une représentation différente de bits. Voilà ce que static_castreprésente.

D'un autre côté, lorsque vous appelez reinterpret_castle CPU , il n'appelle aucun calcul. Il traite simplement un ensemble de bits dans la mémoire comme s'il avait un autre type. Ainsi, lorsque vous convertissez int*en float*avec ce mot clé, la nouvelle valeur (après le déréférencement du pointeur) n'a rien à voir avec l'ancienne valeur au sens mathématique.

Exemple: Il est vrai que cereinterpret_castn'est pas portable pour une raison - l'ordre des octets (endianness). Mais c'est souvent étonnamment la meilleure raison de l'utiliser. Imaginons l'exemple: vous devez lire un nombre binaire 32 bits à partir d'un fichier, et vous savez que c'est du big endian. Votre code doit être générique et fonctionne correctement sur les systèmes big endian (par exemple certains ARM) et little endian (par exemple x86). Vous devez donc vérifier l'ordre des octets. Il est bien connu au moment de la compilation, vous pouvez donc écrire une constexprfonction: Vous pouvez écrire une fonction pour y parvenir:

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

Explication: la représentation binaire dexen mémoire peut être0000'0000'0000'0001(grande) ou0000'0001'0000'0000(peu endienne). Après la réinterprétation, l'octet sous leppointeur peut être respectivement0000'0000ou0000'0001. Si vous utilisez la conversion statique, elle le sera toujours0000'0001, quelle que soit l'endianité utilisée.

ÉDITER:

Dans la première version que j'ai fait par exemple la fonction is_little_endiand'être constexpr. Il compile bien sur le plus récent gcc (8.3.0) mais la norme dit qu'il est illégal. Le compilateur clang refuse de le compiler (ce qui est correct).

jaskmar
la source
1
Bel exemple! Je remplacerais court pour uint16_t et char non signé pour uint8_t pour le rendre moins obscur pour l'homme.
Jan Turoň
@ JanTuroň vrai, nous ne pouvons pas supposer que cela shortprend 16 bits en mémoire. Corrigée.
jaskmar
1
L'exemple est faux. reinterpret_cast n'est pas autorisé dans les fonctions constexpr
Michael Veksler
1
Tout d'abord, ce code est rejeté par les derniers clang (7.0.0) et gcc (8.2.0). Malheureusement, je n'ai pas trouvé la limitation dans le langage formel. Tout ce que j'ai pu trouver était social.msdn.microsoft.com/Forums/vstudio/en-US/…
Michael Veksler
2
Plus spécifiquement, en.cppreference.com/w/cpp/language/constant_expression (item 16) indique clairement que reinterpret_cast ne peut pas être utilisé dans une expression constante. Regardez également github.com/cplusplus/draft/blob/master/papers/N3797.pdf (5.19 expressions constantes) pages125-126 qui exclut explicitement reinterpret_cast. Alors 7.1.5 L' élément 5 du spécificateur constexpr (page 146) * Pour une fonction constexpr non modèle, non par défaut ... s'il n'existe aucune valeur d'argument telle que ... pourrait être une sous-expression évaluée d'une expression constante de base (5.19 ), le programme est mal formé *
Michael Veksler
20

La signification de reinterpret_castn'est pas définie par la norme C ++. Par conséquent, en théorie, un programme reinterpret_castpourrait planter. Dans la pratique, les compilateurs essaient de faire ce que vous attendez, c'est-à-dire d'interpréter les morceaux de ce que vous transmettez comme s'ils étaient du type vers lequel vous diffusez. Si vous savez ce que font les compilateurs que vous allez utiliser, reinterpret_cast vous pouvez l’utiliser, mais pour dire qu’il est portable serait mentir.

Pour le cas que vous décrivez, et à peu près tous les cas où vous pourriez envisager reinterpret_cast, vous pouvez utiliser static_castou une autre alternative à la place. Entre autres choses, la norme a ceci à dire sur ce à quoi vous pouvez vous attendre static_cast(§5.2.9):

Une valeur r de type «pointeur vers cv void» peut être explicitement convertie en pointeur vers le type d'objet. Une valeur de type pointeur vers objet converti en "pointeur vers cv void" et revenant au type de pointeur d'origine aura sa valeur d'origine.

Donc, pour votre cas d'utilisation, il semble assez clair que le comité de normalisation vous a destiné à utiliser static_cast.

flodin
la source
5
Pas tout à fait planter votre programme. La norme offre quelques garanties sur reinterpret_cast. Mais pas autant que les gens s'y attendent souvent.
jalf
1
Pas si vous l'utilisez correctement. Autrement dit, reinterpret_cast de A vers B vers A est parfaitement sûr et bien défini. Mais la valeur de B n'est pas spécifiée, et oui, si vous vous y fiez, de mauvaises choses pourraient se produire. Mais le casting lui-même est suffisamment sûr, tant que vous ne l'utilisez que de la manière permise par la norme. ;)
jalf
55
lol, je soupçonne que reinterpret_crash pourrait effectivement planter votre programme. Mais reinterpret_cast ne le fera pas. ;)
jalf
5
<irony> Je l'ai essayé sur mon compilateur, et en quelque sorte, il a refusé de compiler reinterpret_crash. Aucun bug de compilation ne m'empêchera de planter mon programme de réinterprétation. Je signalerai un bogue dès que possible! </
irony
18
@paercebaltemplate<class T, U> T reinterpret_crash(U a) { return *(T*)nullptr; }
12

Une utilisation de reinterpret_cast est si vous souhaitez appliquer des opérations au niveau du bit aux flottants (IEEE 754). Un exemple de ceci était l'astuce Fast Inverse Square-Root:

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

Il traite la représentation binaire du flottant comme un entier, le déplace vers la droite et le soustrait d'une constante, réduisant ainsi de moitié et annulant l'exposant. Après la reconversion en float, il est soumis à une itération de Newton-Raphson pour rendre cette approximation plus exacte:

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

Cela a été écrit à l'origine en C, donc utilise des transtypages en C, mais le cast C ++ analogue est le reinterpret_cast.

Adam P. Goucher
la source
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
La norme dit qu'il s'agit d'un comportement indéfini: en.cppreference.com/w/cpp/language/reinterpret_cast (sous "type aliasing")
Cris Luengo
@CrisLuengo Si je remplace tout reinterpret_castpar memcpy, est-ce toujours UB?
sandthorn
@sandthorn: Ceci est UB selon la norme, mais si cela fonctionne pour votre architecture, ne vous en faites pas. Cette astuce est OK, je suppose, pour tout compilateur pour les architectures Intel. Cela ne pourrait pas fonctionner comme prévu (ou même planter) sur d'autres architectures - par exemple, il pourrait être possible que les flottants et les longs soient stockés dans des compartiments de mémoire séparés (pas que je sache d'une telle architecture, c'est juste un argument ...) . memcpyserait certainement le rendre légal.
Cris Luengo
2
template <class outType, class inType>
outType safe_cast(inType pointer)
{
    void* temp = static_cast<void*>(pointer);
    return static_cast<outType>(temp);
}

J'ai essayé de conclure et j'ai écrit une distribution simple et sûre à l'aide de modèles. Notez que cette solution ne garantit pas de lancer des pointeurs sur une fonction.

Sasha Zezulinsky
la source
1
Quelle? Pourquoi s'embêter? C'est précisément ce qui se produit reinterpret_castdéjà dans cette situation: "Un pointeur d'objet peut être explicitement converti en un pointeur d'objet d'un type différent. [72] Lorsqu'une valeur v de type de pointeur d'objet est convertie en type de pointeur d'objet" pointer to cv T ", le résultat est static_cast<cv T*>(static_cast<cv void*>(v)). " - N3797.
underscore_d
Quant à la c++2003norme je peux pas trouver que reinterpret_castne faitstatic_cast<cv T*>(static_cast<cv void*>(v))
Sasha Zezulinsky
1
D'accord, c'est vrai, mais je ne me soucie pas d'une version d'il y a 13 ans, et la plupart des codeurs ne devraient pas non plus (comme c'est probable) pouvoir l'éviter. Les réponses et commentaires devraient vraiment refléter la dernière norme disponible, sauf indication contraire ... À mon humble avis. Quoi qu'il en soit, je suppose que le Comité a ressenti le besoin d'ajouter explicitement ceci après 2003. (parce que l'IIRC, c'était la même chose en C ++ 11)
underscore_d
Avant C++03c'était C++98. Des tonnes de projets ont utilisé l'ancien C ++ au lieu du C. portable. Parfois, vous devez vous soucier de la portabilité. Par exemple, vous devez prendre en charge le même code sous Solaris, AIX, HPUX, Windows. En ce qui concerne la dépendance et la portabilité du compilateur, c'est difficile. Donc, un bon exemple d'introduction d'un enfer de portabilité est d'utiliser un reinterpret_castdans votre code
Sasha Zezulinsky
encore une fois, si comme moi, vous êtes heureux de vous limiter uniquement aux plates-formes qui fonctionnent bien avec la dernière et la meilleure version du langage, votre objection est un point discutable.
underscore_d
1

Vous avez d'abord quelques données dans un type spécifique comme int ici:

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

Ensuite, vous voulez accéder à la même variable qu'un autre type comme float: vous pouvez choisir 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

BREF: cela signifie que la même mémoire est utilisée comme un type différent. Vous pouvez donc convertir des représentations binaires de flottants de type int comme ci-dessus en flottants. 0x80000000 est -0 par exemple (la mantisse et l'exposant sont nuls mais le signe, le msb, est un. Cela fonctionne également pour les doubles et les doubles longs.

OPTIMISER: Je pense que reinterpret_cast serait optimisé dans de nombreux compilateurs, tandis que la conversion C est faite par pointerarithmétique (la valeur doit être copiée dans la mémoire, car les pointeurs ne pouvaient pas pointer vers les registres cpu).

REMARQUE: dans les deux cas, vous devez enregistrer la valeur convertie dans une variable avant la conversion! Cette macro pourrait aider:

#define asvar(x) ({decltype(x) __tmp__ = (x); __tmp__; })
cmdLP
la source
Il est vrai que "cela signifie que la même mémoire est utilisée comme un type différent" mais elle est limitée à une paire spécifique de types. Dans votre exemple , sous reinterpret_castforme intde float&est un comportement non défini.
jaskmar
1

Une raison d'utiliser reinterpret_castest lorsqu'une classe de base n'a pas de table virtuelle, mais une classe dérivée en a. Dans ce cas, static_castetreinterpret_cast entraînera des valeurs de pointeur différentes (ce serait le cas atypique mentionné par jalf ci-dessus ). Juste comme un avertissement, je ne dis pas que cela fait partie de la norme, mais la mise en œuvre de plusieurs compilateurs répandus.

À titre d'exemple, prenez le code ci-dessous:

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

Qui produit quelque chose comme:

& b = 0x7ffe10e68b38
a = 0x7ffe10e68b40
ar = 0x7ffe10e68b38
différence = 2

Dans tous les compilateurs que j'ai essayés (MSVC 2015 et 2017, clang 8.0.0, gcc 9.2, icc 19.0.1 - voir godbolt pour les 3 derniers ) le résultat du static_castdiffère de celui du reinterpret_castpar 2 (4 pour MSVC). Le seul compilateur à avertir de la différence était clang, avec:

17:16: avertissement: 'reinterpret_cast' de la classe 'B *' à sa base avec un décalage non nul 'A *' se comporte différemment de 'static_cast' [-Wreinterpret-base-class]
const A * ar = reinterpret_cast (& b) ;
^ ~~~~~~~~~~~~~~~~~~~~~~~
17:16: remarque: utilisez 'static_cast' pour régler correctement le pointeur lors de la conversion ascendante
const A * ar = reinterpret_cast (& b) ;
^ ~~~~~~~~~~~~~~~
static_cast

Une dernière mise en garde est que si la classe de base n'a pas de membres de données (par exemple le int i;) alors clang, gcc et icc renvoient la même adresse pour reinterpret_castque pour static_cast, alors que MSVC n'en a toujours pas.

Avi Ginsburg
la source
1

Voici une variante du programme d'Avi Ginsburg qui illustre clairement la propriété reinterpret_castmentionnée par Chris Luengo, flodin et cmdLP: que le compilateur traite l'emplacement pointé vers la mémoire comme s'il s'agissait d'un objet du nouveau type:

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

Ce qui se traduit par une sortie comme celle-ci:

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.

On peut voir que l'objet B est construit en mémoire en tant que données spécifiques à B d'abord, suivi par l'objet A incorporé. Le static_castrenvoie correctement l'adresse de l'objet A incorporé et le pointeur créé par static_castdonne correctement la valeur du champ de données. Le pointeur généré par les reinterpret_castfriandisesb emplacement de mémoire de comme s'il s'agissait d'un simple objet A, et donc lorsque le pointeur essaie d'obtenir le champ de données, il renvoie des données spécifiques à B comme s'il s'agissait du contenu de ce champ.

Une utilisation de reinterpret_castconsiste à convertir un pointeur en un entier non signé (lorsque les pointeurs et les entiers non signés ont la même taille):

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

TRPh
la source
-6

Réponse rapide: utilisez static_casts'il compile, sinon recourez à reinterpret_cast.

Marius K
la source
-16

Lisez la FAQ ! La conservation des données C ++ en C peut être risquée.

En C ++, un pointeur sur un objet peut être converti en void *sans transtypage. Mais ce n'est pas vrai dans l'autre sens. Vous auriez besoin d'un static_castpour récupérer le pointeur d'origine.

dirkgently
la source