22 votes

Surcharge et polymorphisme des opérateurs de cast C++

Je suis perplexe face à ce comportement du C++ :

struct A {
   virtual void print() const { printf("a\n"); }
};

struct B : public A {
   virtual void print() const { printf("b\n"); }
};

struct C {
   operator B() { return B(); }
};

void print(const A& a) {
   a.print();
}

int main() {
   C c;
   print(c);
}

Le quiz est donc le suivant : quel est le résultat du programme - a ou b ? Eh bien, la réponse est a. Mais pourquoi ?

10voto

David Hammen Points 17912

Le problème est un bogue, un défaut de fonctionnalité ou un trou dans la norme C++03, et les différents compilateurs tentent de corriger le problème de différentes manières. (Ce problème n'existe plus dans la norme C++11).

Les sections 8.5.3/5 des deux normes précisent comment une référence est initialisée. Voici la version C++03 (la numérotation des listes est la mienne) :

Une référence au type cv1 T1 est initialisé par une expression de type cv2 T2 comme suit :

  1. Si l'expression de l'initialisateur

    1. est une lvalue (mais n'est pas un champ de bits), et "cv1 T1" est compatible avec la référence "cv2 T2," ou
    2. a un type de classe (c'est-à-dire, T2 est un type de classe) et peut être implicitement converti en une lvalue de type cv3 T3 , donde cv1 T1 est compatible avec la référence cv3 T3

    alors la référence est liée directement à l'expression lvalue de l'initialisateur dans le premier cas, et la référence est liée au résultat lvalue de la conversion dans le second cas.

  2. Sinon, la référence doit être à un type de const non volatile (c'est-à-dire, cv1 est const ).

  3. Si l'expression de l'initialisateur est une rvalue, avec T2 un type de classe, et cv1 T1 est compatible avec la référence cv2 T2 la référence est liée de l'une des manières suivantes (le choix est défini par l'implémentation) :

    1. La référence est liée à l'objet représenté par la rvalue (voir 3.10) ou à un sous-objet de cet objet.
    2. Un temporaire de type cv1 T2 [est créé, et un constructeur est appelé pour copier l'objet rvalue entier dans le temporaire. La référence est liée au temporaire ou à un sous-objet du temporaire.

    Le constructeur qui serait utilisé pour effectuer la copie doit pouvoir être appelé, que la copie soit effectivement effectuée ou non.

  4. Sinon, un temporaire de type cv1 T1 est créé et initialisé à partir de l'expression de l'initialisateur en utilisant les règles pour une initialisation par copie sans référence (8.5). La référence est ensuite liée au temporaire.

Il y a trois types impliqués dans la question qui nous occupe :

  • Le type de la référence à créer. Les normes (les deux versions) désignent ce type comme suit T1 . Dans ce cas, c'est struct A .
  • Le type de l'expression de l'initialisateur. Les normes désignent ce type comme T2 . Dans ce cas, l'expression initialisatrice est la variable c donc T2 es struct C . Notez que parce que struct A n'est pas compatible avec les références con struct C il n'est pas possible de lier directement la référence à l'adresse de l'utilisateur. c . Un intermédiaire est nécessaire.
  • Le type d'intermédiaire. Les normes désignent ce type comme T3 . Dans ce cas, il s'agit struct B . Notez que l'application de l'opérateur de conversion C::operator B() a c convertira la valeur l c à une rvalue.

Les initialisations par ce que j'ai étiqueté comme 1.1 et 3 sont en dehors parce que la struct A n'est pas compatible avec la référence struct C . L'opérateur de conversion C::operator B() doit être utilisé. 1.2 est exclu Comme cet opérateur de conversion renvoie une valeur r, cela exclut l'option 1.2. Il ne reste que l'option 4, créer un temporaire de type cv1 T1 . Le respect strict de la version 2003 de la norme oblige à créer deux temporaires pour ce problème, alors qu'un seul suffira.

La version 2011 de la norme corrige le problème en remplaçant l'option 3 par

  • Si l'expression de l'initialisateur

    • est une valeur x, une valeur de classe, une valeur de tableau ou une valeur de fonction et cv1 T1 est de référence- compatible avec cv2 T2 ou
    • a un type de classe (c'est-à-dire, T2 est une catégorie de classe), où T1 n'est pas lié à la référence à T2 et peut être implicitement converti en une xvalue, une class prvalue ou une function lvalue de type cv3 T3 , donde cv1 T1 est compatible avec la référence cv3 T3 ,

    alors la référence est liée à la valeur de l'expression initialisatrice dans le premier cas et au résultat de la conversion dans le second cas (ou, dans les deux cas, à un sous-objet de classe de base approprié). Dans le second cas, si la référence est une référence rvalue et que la seconde séquence de conversion standard de la séquence de conversion définie par l'utilisateur comprend une conversion lvalue-to-rvalue, le programme est mal formé.

Il semble que la famille de compilateurs gcc ait choisi la conformité stricte plutôt que l'intention (éviter de créer des temporaires inutiles), tandis que les autres compilateurs qui impriment "b" ont choisi l'intention / les corrections à la norme. Le choix de la conformité stricte n'est pas nécessairement louable ; il y a d'autres bogues/manques de fonctionnalités dans la version 2003 de la norme (par ex, std::set ) où la famille gcc a choisi le bon sens plutôt que la stricte conformité.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X