Quand faut-il utiliser static_cast, dynamic_cast, const_cast et reinterpret_cast?


2496

Quelles sont les utilisations appropriées de:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
  • Fonte de style C (type)value
  • Fonte de style fonctionnel type(value)

Comment décider lequel utiliser dans quels cas spécifiques?



3
Pour quelques exemples concrets utiles d'utilisation de différents types de transtypages, vous pouvez vérifier la première réponse sur une question similaire dans cet autre sujet .
TeaMonkie

2
Vous pouvez trouver de très bonnes réponses à votre question ci-dessus. Mais je voudrais mettre un point de plus ici, @ e.James "Il n'y a rien que ces nouveaux opérateurs de cast c ++ peuvent faire et le cast de style c ne peut pas. Ceux-ci sont ajoutés plus ou moins pour une meilleure lisibilité du code."
BreakBadSP

@BreakBadSP Les nouvelles conversions ne sont pas seulement pour une meilleure lisibilité du code. Ils sont là pour rendre plus difficile de faire des choses dangereuses, comme jeter des const ou lancer des pointeurs au lieu de leurs valeurs. static_cast a beaucoup moins de possibilités de faire quelque chose de dangereux que le style AC!
FourtyTwo

@FourtyTwo a accepté
BreakBadSP

Réponses:


2571

static_castest la première distribution que vous devriez essayer d'utiliser. Il fait des choses comme des conversions implicites entre les types (comme intvers floatou pointeur vers void*), et il peut également appeler des fonctions de conversion explicites (ou implicites). Dans de nombreux cas, une explication explicite static_castn'est pas nécessaire, mais il est important de noter que la T(something)syntaxe est équivalente (T)somethinget doit être évitée (plus à ce sujet plus tard). A T(something, something_else)est cependant sûr et garanti d'appeler le constructeur.

static_castpeut également transtyper les hiérarchies d'héritage. Elle n'est pas nécessaire lors d'une conversion vers le haut (vers une classe de base), mais lors d'une conversion vers le bas, elle peut être utilisée tant qu'elle ne transite pas par virtualhéritage. Il ne fait pas de vérification, cependant, et c'est un comportement indéfini de static_castdescendre une hiérarchie vers un type qui n'est pas réellement le type de l'objet.


const_castpeut être utilisé pour supprimer ou ajouter constà une variable; aucun autre cast C ++ n'est capable de le supprimer (pas même reinterpret_cast). Il est important de noter que la modification d'une ancienne constvaleur n'est indéfinie que si la variable d'origine l'est const; si vous l'utilisez pour constenlever une référence à quelque chose qui n'a pas été déclaré const, c'est sûr. Cela peut être utile lors de la surcharge de fonctions membres basées sur const, par exemple. Il peut également être utilisé pour ajouter constà un objet, par exemple pour appeler une surcharge de fonction membre.

const_castfonctionne également de la même manière volatile, bien que ce soit moins courant.


dynamic_castest exclusivement utilisé pour gérer le polymorphisme. Vous pouvez convertir un pointeur ou une référence à tout type polymorphe en tout autre type de classe (un type polymorphe a au moins une fonction virtuelle, déclarée ou héritée). Vous pouvez l'utiliser pour plus que simplement lancer vers le bas - vous pouvez lancer sur le côté ou même sur une autre chaîne. Le dynamic_castcherchera l'objet souhaité et le restituera si possible. Si ce n'est pas le cas, il reviendra nullptrdans le cas d'un pointeur, ou sera renvoyé std::bad_castdans le cas d'une référence.

dynamic_casta cependant quelques limites. Cela ne fonctionne pas s'il y a plusieurs objets du même type dans la hiérarchie d'héritage (le soi-disant «diamant redouté») et que vous n'utilisez pas l' virtualhéritage. Il ne peut également passer que par héritage public - il échouera toujours à traverser protectedou privatehériter. Il s'agit cependant rarement d'un problème, car de telles formes d'héritage sont rares.


reinterpret_castest le plâtre le plus dangereux et doit être utilisé avec parcimonie. Il transforme un type directement en un autre - comme la conversion de la valeur d'un pointeur en un autre, ou le stockage d'un pointeur dans une int, ou toutes sortes d'autres choses désagréables. Généralement, la seule garantie que vous obtenez reinterpret_castest que, normalement, si vous restituez le résultat au type d'origine, vous obtiendrez la même valeur exacte (mais pas si le type intermédiaire est plus petit que le type d'origine). Il existe également un certain nombre de conversions qui reinterpret_castne peuvent pas le faire. Il est principalement utilisé pour des conversions et des manipulations de bits particulièrement étranges, comme transformer un flux de données brutes en données réelles ou stocker des données dans les bits faibles d'un pointeur vers des données alignées.


Les castes de style C et les castes de style fonctionnel sont des castes utilisant (type)objectou type(object), respectivement, et sont fonctionnellement équivalentes. Ils sont définis comme le premier des éléments suivants qui réussit:

  • const_cast
  • static_cast (mais en ignorant les restrictions d'accès)
  • static_cast (voir ci-dessus), puis const_cast
  • reinterpret_cast
  • reinterpret_cast, puis const_cast

Il peut donc être utilisé en remplacement d'autres lancers dans certains cas, mais peut être extrêmement dangereux en raison de la capacité de se transformer en un reinterpret_cast, et ce dernier devrait être préféré lorsqu'une conversion explicite est nécessaire, sauf si vous êtes sûr de static_castréussir ou d' reinterpret_castéchouer. . Même alors, considérez l'option plus longue et plus explicite.

Les conversions de style C ignorent également le contrôle d'accès lors de l'exécution d'un static_cast, ce qui signifie qu'elles ont la possibilité d'effectuer une opération qu'aucune autre distribution ne peut faire. C'est surtout un coup de coude, cependant, et dans mon esprit, c'est juste une autre raison d'éviter les moulages de style C.


17
dynamic_cast est uniquement pour les types polymorphes. vous ne devez l'utiliser que lorsque vous effectuez un cast vers une classe dérivée. static_cast est certainement la première option, sauf si vous avez spécifiquement besoin de la fonctionnalité de dynamic_cast. Ce n'est pas un miracle miracle "casting de vérification de type" en général.
1er

2
Très bonne réponse! Une remarque rapide: static_cast peut être nécessaire pour remonter la hiérarchie dans le cas où vous avez un Derived * & pour lancer dans Base * &, car les doubles pointeurs / références ne remontent pas automatiquement la hiérarchie. Je suis tombé sur une telle situation (franchement, pas courante) il y a deux minutes. ;-)
bartgol

5
* "aucune autre distribution C ++ n'est capable de supprimer const(même pas reinterpret_cast)" ... vraiment? Et alors reinterpret_cast<int *>(reinterpret_cast<uintptr_t>(static_cast<int const *>(0)))?
user541686

29
Je pense qu'un détail important manquant ci-dessus est que dynamic_cast a une pénalité de performance d'exécution par rapport à static ou reinterpret_cast. Ceci est important, par exemple dans les logiciels en temps réel.
jfritz42

5
Il convient de mentionner que reinterpret_castc'est souvent l'arme de choix lorsqu'il s'agit d'un ensemble de types de données opaques pour une API
Class Skeleton

333

À utiliser dynamic_castpour convertir des pointeurs / références dans une hiérarchie d'héritage.

À utiliser static_castpour les conversions de type ordinaire.

À utiliser reinterpret_castpour la réinterprétation de bas niveau des modèles de bits. À utiliser avec une extrême prudence.

Utilisez const_castpour jeter const/volatile. Évitez cela, sauf si vous êtes bloqué en utilisant une API const-incorrecte.


2
Soyez prudent avec dynamic_cast. Il repose sur RTTI et cela ne fonctionnera pas comme prévu au-delà des limites des bibliothèques partagées. Tout simplement parce que vous construisez une bibliothèque exécutable et partagée de manière indépendante, il n'existe aucun moyen standardisé de synchroniser RTTI entre différentes versions. Pour cette raison, dans la bibliothèque Qt, il existe qobject_cast <> qui utilise les informations de type QObject pour vérifier les types.
user3150128

198

(Beaucoup d'explications théoriques et conceptuelles ont été données ci-dessus)

Voici quelques - uns des exemples pratiques quand je static_cast , dynamic_cast , const_cast , reinterpret_cast .

(Se réfère également à cela pour comprendre l'explication: http://www.cplusplus.com/doc/tutorial/typecasting/ )

static_cast:

OnEventData(void* pData)

{
  ......

  //  pData is a void* pData, 

  //  EventData is a structure e.g. 
  //  typedef struct _EventData {
  //  std::string id;
  //  std:: string remote_id;
  //  } EventData;

  // On Some Situation a void pointer *pData
  // has been static_casted as 
  // EventData* pointer 

  EventData *evtdata = static_cast<EventData*>(pData);
  .....
}

dynamic_cast:

void DebugLog::OnMessage(Message *msg)
{
    static DebugMsgData *debug;
    static XYZMsgData *xyz;

    if(debug = dynamic_cast<DebugMsgData*>(msg->pdata)){
        // debug message
    }
    else if(xyz = dynamic_cast<XYZMsgData*>(msg->pdata)){
        // xyz message
    }
    else/* if( ... )*/{
        // ...
    }
}

const_cast:

// *Passwd declared as a const

const unsigned char *Passwd


// on some situation it require to remove its constness

const_cast<unsigned char*>(Passwd)

reinterpret_cast:

typedef unsigned short uint16;

// Read Bytes returns that 2 bytes got read. 

bool ByteBuffer::ReadUInt16(uint16& val) {
  return ReadBytes(reinterpret_cast<char*>(&val), 2);
}

31
La théorie de certaines des autres réponses est bonne, mais toujours confuse, voir ces exemples après avoir lu les autres réponses leur donne vraiment du sens. C'est sans les exemples, je n'étais toujours pas sûr, mais avec eux, je suis maintenant sûr de la signification des autres réponses.
Solx

1
À propos de la dernière utilisation de reinterpret_cast: n'est-ce pas la même chose que l'utilisation static_cast<char*>(&val)?
Lorenzo Belli

3
@LorenzoBelli Bien sûr que non. L'avez-vous essayé? Ce dernier n'est pas valide en C ++ et bloque la compilation. static_castne fonctionne qu'entre types avec des conversions définies, relation visible par héritage ou vers / depuis void *. Pour tout le reste, il existe d'autres modèles. reinterpret castà n'importe quel char *type est autorisé pour permettre la lecture de la représentation de n'importe quel objet - et l'un des seuls cas où ce mot-clé est utile, pas un générateur rampant de mise en œuvre / comportement indéfini. Mais cela n'est pas considéré comme une conversion «normale», ce qui n'est pas autorisé par les (généralement) très conservateurs static_cast.
underscore_d

2
reinterpret_cast est assez courant lorsque vous travaillez avec des logiciels système tels que des bases de données. Dans la plupart des cas, vous écrivez votre propre gestionnaire de pages qui n'a aucune idée du type de données stocké dans la page et renvoie simplement un pointeur vide. C'est aux niveaux supérieurs de faire une distribution réinterprétée et de la déduire comme ils veulent.
Sohaib

1
L'exemple const_cast présente un comportement indéfini. Une variable déclarée const ne peut pas être déconstruite. Cependant, une variable déclarée comme non-const qui est passée à une fonction prenant une référence const peut dans cette fonction être dé-constée sans qu'elle soit UB.
Johann Gerell

99

Cela pourrait aider si vous connaissez peu les éléments internes ...

static_cast

  • Le compilateur C ++ sait déjà comment convertir entre des types de scaler tels que float en int. Utilisez static_castpour eux.
  • Lorsque vous demandez au compilateur de convertir de type Aen B, le constructeur d' static_castappels Bpasse Aen paramètre. Alternativement, Apourrait avoir un opérateur de conversion (ie A::operator B()). Si Bn'a pas un tel constructeur ou An'a pas d'opérateur de conversion, vous obtenez une erreur de temps de compilation.
  • La conversion de A*à B*réussit toujours si A et B sont dans la hiérarchie d'héritage (ou nuls) sinon vous obtenez une erreur de compilation.
  • Gotcha : Si vous convertissez le pointeur de base en pointeur dérivé mais si l'objet réel n'est pas vraiment un type dérivé, vous n'obtiendrez pas d' erreur. Vous obtenez un mauvais pointeur et très probablement un défaut de segmentation lors de l'exécution. Même chose pour A&la B&.
  • Gotcha : Cast de Derived à Base ou viceversa crée une nouvelle copie! Pour les personnes venant de C # / Java, cela peut être une énorme surprise car le résultat est essentiellement un objet coupé créé à partir de Derived.

dynamic_cast

  • dynamic_cast utilise les informations de type d'exécution pour déterminer si la conversion est valide. Par exemple, (Base*)to (Derived*)peut échouer si le pointeur n'est pas réellement de type dérivé.
  • Cela signifie que dynamic_cast est très cher par rapport à static_cast!
  • Pour A*to B*, si cast n'est pas valide, dynamic_cast renverra nullptr.
  • Pour A&de B&si coulée est invalide, dynamic_cast jetteront exception bad_cast.
  • Contrairement aux autres transtypages, il y a une surcharge d'exécution.

const_cast

  • Bien que static_cast puisse faire non const pour const, il ne peut pas faire autrement. Le const_cast peut faire dans les deux sens.
  • Un exemple où cela est pratique est l'itération à travers un conteneur comme celui set<T>qui ne renvoie que ses éléments sous forme de const pour vous assurer que vous ne changez pas sa clé. Cependant, si votre intention est de modifier les membres non clés de l'objet, cela devrait être correct. Vous pouvez utiliser const_cast pour supprimer la constance.
  • Un autre exemple est lorsque vous souhaitez mettre en œuvre T& SomeClass::foo()ainsi que const T& SomeClass::foo() const. Pour éviter la duplication de code, vous pouvez appliquer const_cast pour renvoyer la valeur d'une fonction d'une autre.

reinterpret_cast

  • Cela dit essentiellement que prenez ces octets à cet emplacement de mémoire et considérez-le comme un objet donné.
  • Par exemple, vous pouvez charger 4 octets de float à 4 octets d'int pour voir à quoi ressemblent les bits de float.
  • Évidemment, si les données ne sont pas correctes pour le type, vous pouvez obtenir une erreur de segmentation.
  • Il n'y a pas de surcharge d'exécution pour cette distribution.

J'ai ajouté les informations de l'opérateur de conversion, mais il y a quelques autres choses qui devraient également être corrigées et je ne me sens pas trop à l'aise de les mettre à jour. Les éléments sont: 1. If you cast base pointer to derived pointer but if actual object is not really derived type then you don't get error. You get bad pointer and segfault at runtime.Vous obtenez UB qui peut entraîner un défaut de segmentation lors de l'exécution si vous êtes chanceux. 2. Les moulages dynamiques peuvent également être utilisés pour la coulée croisée. 3. Dans certains cas, les lancers const peuvent entraîner une UB. L'utilisation mutablepeut être un meilleur choix pour implémenter la constance logique.
Adrian

1
@Adrian vous avez raison à tous points de vue. La réponse est écrite pour les gens de niveau plus ou moins débutant et je ne voulais pas les submerger par toutes les autres complications qui en mutable
découlent

16

Est- ce que cela répond à votre question?

Je ne l'ai jamais utilisé reinterpret_castet je me demande si le fait de rencontrer un boîtier qui en a besoin n'est pas une odeur de mauvaise conception. Dans la base de code sur laquelle je travaille dynamic_castest beaucoup utilisé. La différence avec static_castest qu'une dynamic_castvérification de l'exécution ne peut (plus sûre) ou pas (plus de surcharge) être ce que vous voulez (voir msdn ).


3
J'ai utilisé reintrepret_cast dans un seul but - extraire les bits d'un double (même taille aussi longtemps sur ma plate-forme).
Joshua

2
reinterpret_cast est nécessaire, par exemple pour travailler avec des objets COM. CoCreateInstance () a un paramètre de sortie de type void ** (le dernier paramètre), dans lequel vous passerez votre pointeur déclaré comme par exemple "INetFwPolicy2 * pNetFwPolicy2". Pour ce faire, vous devez écrire quelque chose comme reinterpret_cast <void **> (& pNetFwPolicy2).
Serge Rogatch

1
Il existe peut-être une approche différente, mais j'utilise reinterpret_castpour extraire des éléments de données d'un tableau. Par exemple, si j'ai un char*contenant un grand tampon plein de données binaires compressées que je dois parcourir et obtenir des primitives individuelles de types différents. Quelque chose comme ça:template<class ValType> unsigned int readValFromAddress(char* addr, ValType& val) { /*On platforms other than x86(_64) this could do unaligned reads, which could be bad*/ val = (*(reinterpret_cast<ValType*>(addr))); return sizeof(ValType); }
James Matta

Je ne l'ai jamais utilisé reinterpret_cast, il n'y a pas beaucoup d'utilisations.
Pika le magicien des baleines du

Personnellement, je ne l'ai jamais vu reinterpret_castutilisé que pour une seule raison. J'ai vu des données d'objet brutes stockées dans un type de données "blob" dans une base de données, puis lorsque les données sont extraites de la base de données, elles reinterpret_castsont utilisées pour transformer ces données brutes en objet.
ImaginaryHuman072889

15

En plus des autres réponses jusqu'à présent, voici un exemple non évident où ce static_castn'est pas suffisant, ce qui reinterpret_castest nécessaire. Supposons qu'il existe une fonction qui, dans un paramètre de sortie, renvoie des pointeurs vers des objets de classes différentes (qui ne partagent pas une classe de base commune). Un exemple réel d'une telle fonction est CoCreateInstance()(voir le dernier paramètre, qui est en fait void**). Supposons que vous demandiez une classe d'objet particulière à cette fonction, afin de connaître à l'avance le type du pointeur (ce que vous faites souvent pour les objets COM). Dans ce cas, vous ne pouvez pas convertir le pointeur vers votre pointeur void**avec static_cast: vous en avez besoin reinterpret_cast<void**>(&yourPointer).

Dans du code:

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

Cependant, static_castfonctionne pour des pointeurs simples (pas des pointeurs vers des pointeurs), donc le code ci-dessus peut être réécrit pour éviter reinterpret_cast(au prix d'une variable supplémentaire) de la manière suivante:

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

Cela ne fonctionnerait-il pas quelque chose comme &static_cast<void*>(pNetFwPolicy2)au lieu de static_cast<void**>(&pNetFwPolicy2)?
jp48

9

Alors que d'autres réponses décrivaient bien toutes les différences entre les conversions C ++, je voudrais ajouter une courte note pourquoi vous ne devriez pas utiliser les conversions de style C (Type) varet Type(var).

Pour les débutants C ++, les conversions de style C ressemblent à l'opération de sur-ensemble par rapport aux conversions C ++ (static_cast <> (), dynamic_cast <> (), const_cast <> (), reinterpret_cast <> ()) et quelqu'un pourrait les préférer aux conversions C ++ . En fait, le casting de style C est le sur-ensemble et le plus court à écrire.

Le principal problème des castes de style C est qu'elles cachent l'intention réelle du développeur de la distribution. Les transtypages de style C peuvent effectuer pratiquement tous les types de transtypage, des transtypages normalement sécurisés effectués par static_cast <> () et dynamic_cast <> () aux transtypages potentiellement dangereux comme const_cast <> (), où le modificateur const peut être supprimé de sorte que les variables const peut être modifié et reinterpret_cast <> () qui peut même réinterpréter des valeurs entières vers des pointeurs.

Voici l'exemple.

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.

La raison principale pour laquelle les castings C ++ ont été ajoutés au langage était de permettre à un développeur de clarifier ses intentions - pourquoi il va faire ce cast. En utilisant des conversions de style C qui sont parfaitement valides en C ++, vous rendez votre code moins lisible et plus sujet aux erreurs, en particulier pour les autres développeurs qui n'ont pas créé votre code. Donc, pour rendre votre code plus lisible et explicite, vous devriez toujours préférer les transtypages C ++ aux transtypages de style C.

Voici une courte citation du livre de Bjarne Stroustrup (l'auteur de C ++) The C ++ Programming Language 4th edition - page 302.

Cette distribution de style C est beaucoup plus dangereuse que les opérateurs de conversion nommés car la notation est plus difficile à repérer dans un grand programme et le type de conversion prévu par le programmeur n'est pas explicite.


5

Pour comprendre, considérons l'extrait de code ci-dessous:

struct Foo{};
struct Bar{};

int main(int argc, char** argv)
{
    Foo* f = new Foo;

    Bar* b1 = f;                              // (1)
    Bar* b2 = static_cast<Bar*>(f);           // (2)
    Bar* b3 = dynamic_cast<Bar*>(f);          // (3)
    Bar* b4 = reinterpret_cast<Bar*>(f);      // (4)
    Bar* b5 = const_cast<Bar*>(f);            // (5)

    return 0;
}

Seule la ligne (4) se compile sans erreur. Seul reinterpret_cast peut être utilisé pour convertir un pointeur vers un objet en un pointeur vers n'importe quel type d'objet sans rapport.

Une chose à noter est la suivante: le dynamic_cast échouerait au moment de l'exécution, mais sur la plupart des compilateurs, il ne parviendra pas non plus à compiler car il n'y a pas de fonctions virtuelles dans la structure du pointeur en cours de conversion, ce qui signifie que dynamic_cast ne fonctionnera qu'avec des pointeurs de classe polymorphes. .

Quand utiliser le cast C ++ :

  • Utilisez static_cast comme l'équivalent d'une conversion de style C qui effectue une conversion de valeur, ou lorsque nous devons explicitement convertir un pointeur d'une classe vers sa superclasse.
  • Utilisez const_cast pour supprimer le qualificatif const.
  • Utilisez reinterpret_cast pour effectuer des conversions non sécurisées de types de pointeurs vers et à partir d'entiers et d'autres types de pointeurs. N'utilisez ceci que si nous savons ce que nous faisons et si nous comprenons les problèmes d'alias.

3

static_castvs dynamic_castvs reinterpret_castinternals voir sur un downcast / upcast

Dans cette réponse, je veux comparer ces trois mécanismes sur un exemple concret upcast / downcast et analyser ce qui arrive aux pointeurs / mémoire / assemblage sous-jacents pour donner une compréhension concrète de la façon dont ils se comparent.

Je crois que cela donnera une bonne intuition sur la façon dont ces lancers sont différents:

  • static_cast: une adresse est-elle décalée lors de l'exécution (faible impact sur l'exécution) et aucune vérification de la sécurité de la correction d'un downcast n'est correcte.

  • dyanamic_cast: fait le même décalage d'adresse lors de l'exécution comme static_cast, mais aussi et une vérification de sécurité coûteuse qu'un downcast est correct en utilisant RTTI.

    Cette vérification de sécurité vous permet de rechercher si un pointeur de classe de base est d'un type donné au moment de l'exécution en vérifiant un retour nullptrqui indique un abaissement invalide.

    Par conséquent, si votre code n'est pas en mesure de vérifier cela nullptret de prendre une action non abandonnée valide, vous devez simplement utiliser à la static_castplace de la conversion dynamique.

    Si un abandon est la seule action que votre code peut entreprendre, peut-être voulez-vous uniquement activer les dynamic_castbuilds de débogage ( -NDEBUG), et utiliser static_castautrement, par exemple comme ici , pour ne pas ralentir vos exécutions rapides.

  • reinterpret_cast: ne fait rien à l'exécution, pas même le décalage d'adresse. Le pointeur doit pointer exactement vers le type correct, même une classe de base ne fonctionne pas. Vous ne voulez généralement pas cela à moins que des flux d'octets bruts ne soient impliqués.

Prenons l'exemple de code suivant:

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

Compilez, exécutez et démontez avec:

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

setarchest utilisé pour désactiver ASLR pour faciliter la comparaison des exécutions.

Sortie possible:

&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

Maintenant, comme mentionné sur: https://en.wikipedia.org/wiki/Virtual_method_table afin de prendre en charge efficacement les appels de méthode virtuelle, la structure des données de la mémoire de Ddoit ressembler à:

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

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

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

Le fait essentiel est que la structure de données de mémoire de Dcontient à l'intérieur une structure de mémoire compatible avec celle de B1et celle de l' B2intérieur.

Par conséquent, nous arrivons à la conclusion critique:

un upcast ou downcast n'a besoin que de déplacer la valeur du pointeur par une valeur connue au moment de la compilation

De cette façon, lorsqu'il Dest passé au tableau de types de base, le transtypage de type calcule réellement ce décalage et pointe quelque chose qui ressemble exactement à un valide B2en mémoire:

b2s[1] = &d;

sauf que celui-ci a la vtable pour Dau lieu de B2, et donc tous les appels virtuels fonctionnent de manière transparente.

Maintenant, nous pouvons enfin revenir au type de coulée et à l'analyse de notre exemple concret.

De la sortie standard, nous voyons:

&d           0x7fffffffc930
b2s[1]       0x7fffffffc940

Par conséquent, l'implicite static_castfait là a correctement calculé le décalage entre la Dstructure de données complète à 0x7fffffffc930 et celle B2similaire qui est à 0x7fffffffc940. Nous déduisons également que ce qui se situe entre 0x7fffffffc930 et 0x7fffffffc940 est probablement les B1données et la table.

Ensuite, dans les sections downcast, il est maintenant facile de comprendre comment les invalides échouent et pourquoi:

  • static_cast<D*>(b2s[0]) 0x7fffffffc910: le compilateur vient de monter 0x10 lors de la compilation des octets pour essayer de passer de a B2à contenantD

    Mais comme ce b2s[0]n'était pas le cas D, il pointe désormais vers une région de mémoire non définie.

    Le démontage est:

    49          dp = static_cast<D*>(b2s[0]);
       0x0000000000000fc8 <+414>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x0000000000000fcc <+418>:   48 85 c0        test   %rax,%rax
       0x0000000000000fcf <+421>:   74 0a   je     0xfdb <main()+433>
       0x0000000000000fd1 <+423>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x0000000000000fd5 <+427>:   48 83 e8 10     sub    $0x10,%rax
       0x0000000000000fd9 <+431>:   eb 05   jmp    0xfe0 <main()+438>
       0x0000000000000fdb <+433>:   b8 00 00 00 00  mov    $0x0,%eax
       0x0000000000000fe0 <+438>:   48 89 45 98     mov    %rax,-0x68(%rbp)

    nous voyons donc que GCC fait:

    • vérifier si le pointeur est NULL, et si oui, retourner NULL
    • sinon, en soustrayez 0x10 pour atteindre le Dqui n'existe pas
  • dynamic_cast<D*>(b2s[0]) 0: C ++ a effectivement trouvé que la distribution n'était pas valide et est retournée nullptr!

    Il n'y a aucun moyen que cela puisse être fait au moment de la compilation, et nous confirmerons que dès le démontage:

    59          dp = dynamic_cast<D*>(b2s[0]);
       0x00000000000010ec <+706>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x00000000000010f0 <+710>:   48 85 c0        test   %rax,%rax
       0x00000000000010f3 <+713>:   74 1d   je     0x1112 <main()+744>
       0x00000000000010f5 <+715>:   b9 10 00 00 00  mov    $0x10,%ecx
       0x00000000000010fa <+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,%rdi
       0x000000000000110b <+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 00  mov    $0x0,%eax
       0x0000000000001117 <+749>:   48 89 45 98     mov    %rax,-0x68(%rbp)

    Il y a d'abord une vérification NULL, et elle retourne NULL si l'entrée est NULL.

    Sinon, il configure certains arguments dans RDX, RSI et RDI et appelle __dynamic_cast.

    Je n'ai pas la patience d'analyser cela plus en détail maintenant, mais comme d'autres l'ont dit, la seule façon pour que cela fonctionne est __dynamic_castd'accéder à des structures de données en mémoire RTTI supplémentaires qui représentent la hiérarchie des classes.

    Il faut donc commencer à partir de l' B2entrée de cette table, puis marcher cette hiérarchie de classe jusqu'à ce qu'il trouve que le vtable pour un Dtypecast de b2s[0].

    C'est pourquoi réinterpréter le casting est potentiellement cher! Voici un exemple où un patch à une ligne convertissant un dynamic_casten un static_castdans un projet complexe a réduit le temps d'exécution de 33%! .

  • reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940celui-ci nous croit juste aveuglément: nous avons dit qu'il y a une Dadresse à b2s[1], et le compilateur ne fait aucun calcul de décalage.

    Mais c'est faux, car D est en fait à 0x7fffffffc930, ce qui est à 0x7fffffffc940 est la structure de type B2 à l'intérieur de D! La corbeille est donc accessible.

    Nous pouvons le confirmer de l' -O0assemblée horrible qui déplace simplement la valeur:

    70          dp = reinterpret_cast<D*>(b2s[1]);
       0x00000000000011fa <+976>:   48 8b 45 d8     mov    -0x28(%rbp),%rax
       0x00000000000011fe <+980>:   48 89 45 98     mov    %rax,-0x68(%rbp)

Questions connexes:

Testé sur Ubuntu 18.04 amd64, GCC 7.4.0.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.