Pourquoi les fonctions virtuelles ne peuvent-elles pas utiliser la déduction de type de retour?

N3797 dit:

§ 7.1.6.4/14:

Une fonction déclarée avec un type de retour utilisant un type d’espace réservé ne doit pas être virtuelle (10.3).

Par conséquent, le programme suivant est mal formé:

struct s { virtual auto foo() { } }; 

Tout ce que je peux trouver pour la raison est cette vague one-liner de n3638 :

virtuel

Il serait possible d’autoriser la déduction de type de retour pour les fonctions virtuelles, mais cela compliquerait à la fois le contrôle de substitution et la présentation de vtable, il semble donc préférable de l’interdire.

Quelqu’un peut-il fournir une justification supplémentaire ou donner un bon exemple (de code) en accord avec la citation ci-dessus?

La logique que vous avez incluse est assez claire: naturellement, les fonctions virtuelles doivent être remplacées par des sous-classes. Par conséquent, en tant que concepteur de la classe de base, vous permettez aux personnes qui héritent de votre classe de fournir le remplacement approprié. Toutefois, si vous utilisez auto , déterminer le type de retour pour la substitution devient une tâche fastidieuse pour un programmeur. Les compilateurs auraient moins de problèmes avec cela, mais les humains auraient de nombreuses occasions de se perdre.

Par exemple, si vous voyez une instruction return qui ressemble à ceci

 return a * 3 + b; 

vous devez remonter le programme jusqu’au sharepoint déclaration de a et b , déterminer le type de promotions et décider du type de retour.

Il semble que les concepteurs de langage aient compris que cela serait assez déroutant et ont décidé de ne pas autoriser cette fonctionnalité.

Le type de retour déduit de la fonction n’est connu qu’au moment de la définition de la fonction: le type de retour est déduit des instructions de return figurant dans le corps de la fonction.

Pendant ce temps, la vtable est construite et la sémantique redéfinie est vérifiée uniquement sur la base des déclarations de fonction présentes dans la définition de la classe. Ces vérifications ne s’appuyaient jamais sur la définition de la fonction et n’avaient jamais besoin de voir la définition. Par exemple, la langue requirejs que la fonction de remplacement ait le même type de retour ou un type de retour covariant que la fonction qu’il substitue. Lorsque la déclaration de fonction non définissante spécifie un type de retour déduit (c’est-à auto dire auto sans type de retour final), son type de retour est inconnu à ce stade et le rest jusqu’à ce que le compilateur rencontre la définition de la fonction. Il n’est pas possible d’effectuer la vérification du type de retour susmentionnée lorsque le type de retour est inconnu. Demander au compilateur de reporter d’une manière ou d’une autre la vérification du type de retour au point où cela devient connu nécessiterait une refonte qualitative majeure de ce domaine fondamental de la spécification de langage. (Je ne suis pas sûr que ce soit même possible.)

Une autre solution consisterait à décharger le compilateur de ce fardeau en vertu du mandat général “aucun diagnostic n’est requirejs” ou “le comportement n’est pas défini”, c’est-à-dire que la responsabilité est transférée à l’utilisateur, mais cela constituerait également un écart majeur par rapport à l’ancien. conception de la langue.

Fondamentalement, pour une raison un peu similaire, vous ne pouvez pas appliquer l’opérateur & à une fonction déclarée comme auto f(); mais pas encore défini, comme le montre l’exemple du 7.1.6.3/11.

auto est un type inconnu dans une équation de type; comme d’habitude, le type devrait être défini à un moment donné. Une fonction virtuelle doit avoir une définition, elle est toujours “utilisée” même si la fonction n’est jamais appelée dans le programme.

Brève description du problème de vtable

Les types de retour covariants sont un problème d’implémentation de vtable: les retours covariants sont une fonctionnalité puissante en interne (puis castrés par des règles de langage arbitraires). La covariance est limitée aux pointeurs (et références) dérivés des conversions de base, mais le pouvoir interne et donc la difficulté d’implémentation est presque celui des conversions arbitraires: dérivé de base montant en code arbitraire (dérivé de base limité à des sous-objects exclusifs de la classe de base, aka inheritance non virtuel, serait beaucoup plus simple).

La covariance en cas de conversion en sous-objects de base partagés (inheritance virtuel) signifie que la conversion peut non seulement modifier la représentation de la valeur du pointeur, mais également sa valeur de manière à perdre de l’information, dans le cas général.

De ce fait, la covariance virtuelle (type de retour covariant impliquant une conversion d’inheritance virtuel) signifie que le remplaçant ne peut pas être confondu avec la fonction remplacée dans une situation de base primaire.

Explication détaillée

Théorie de base des tables vtables et primaires

 struct Primbase { virtual void foo(); // new }; struct Der : Primbase { // primary base void foo(); // replace Primbase::foo() virtual void bar(); // new slot }; 

Primbase est la base principale ici, elle commence à la même adresse que l’object dérivé. Ceci est extrêmement important: pour la base primaire, les conversions ascendante / descendante peuvent être effectuées avec une réinterprétation ou une conversion de style C dans le code généré. L’inheritance simple est tellement plus facile pour l’implémenteur car il n’y a que des classes de base primaires. Avec l’inheritance multiple, l’arithmétique de pointeur est nécessaire.

Il n’y a qu’un seul vptr dans Der , celui de Primbase ; il y a une vtable pour Der , la mise en page compatible avec la vtable de Primbase .

Ici, le compilateur habituel n’allouera pas un autre emplacement pour Der::foo() dans la table vtable, car la fonction dérivée est appelée (dans l’hypothèse du code C généré) avec un Primbase* this pointeur, pas un Der* . La table virtuelle n’a que deux emplacements (plus les données RTTI).

Covariance primaire

Ajoutons maintenant une simple covariance:

 struct Primbase { virtual Primbase *foo(); // new slot in vtable }; struct Der : Primbase { // primary base Der *foo(); // replaces Primbase::foo() in vtable virtual void bar(); // new slot }; 

Ici, la covariance est sortingviale, car elle implique une base primaire. Rien à voir au niveau du code compilé.

Covariance de décalage non nulle

Plus complexe:

 struct Basebelow { virtual void bar(); // new slot }; struct Primbase { virtual Basebelow *foo(); // new }; struct Der : Primbase, // primary base Basebelow { // base at a non zero offset Der *foo(); // new slot? }; 

Ici, la représentation d’un Der* n’est pas la même que celle du pointeur de sous-object de la classe de base Basebelow* . Deux choix d’implémentations:

  • (régler) régler sur l’interface d’appel virtuelle Basebelow *(Primbase::foo)() pour l’ensemble de la hiérarchie: this s’agit d’une Primbase* (compatible avec Der* ) mais le type de valeur renvoyé n’est pas compatible (représentation différente); L’implémentation de la fonction convertira le Der* en Primbase* (arithmétique de pointeur) et l’appelant avec reconvertir lors d’un appel virtuel sur un Der ;

  • (introduisez) un autre emplacement de fonction virtuel dans Der vtable pour la fonction renvoyant un Der* .

Généralisé dans une hiérarchie de partage: covariance virtuelle

Dans le cas général, les sous-objects de la classe de base sont partagés par différentes classes dérivées, il s’agit d’un “losange” virtuel:

 struct B {}; struct L : virtual B {}; struct R : virtual B {}; struct D : L, R {}; 

Ici, la conversion en B* est dynamic, basée sur le type d’exécution (utilisant souvent le vptr, ou bien des pointeurs / décalages internes dans les objects, comme dans MSVC).

En général, de telles conversions en sous-object de classe de base perdent des informations et ne peuvent pas être annulées. Il n’existe pas de conversion fiable de B* à L* down. Par conséquent, le choix (régler) n’est pas disponible. La mise en œuvre devra (introduire) .

Exemple: Vtable pour une substitution avec un type de retour covariant dans l’ABI Itanium

L’IBI C ++ ABI décrit la mise en page de la vtable . Voici la règle concernant l’introduction d’entrées vtable pour une classe dérivée (en particulier une classe de base primaire):

Il existe une entrée pour toute fonction virtuelle déclarée dans une classe, qu’il s’agisse d’une nouvelle fonction ou d’une fonction de classe de base, à moins qu’elle ne remplace une fonction de la base primaire et que la conversion entre leurs types de retour ne nécessite aucun ajustement .

(c’est moi qui souligne)

Ainsi, lorsqu’une fonction remplace une déclaration dans la classe de base, le type de retour est comparé: si elles sont similaires, c’est-à-dire que l’une est invariablement une classe de base primaire de l’autre, en d’autres termes, toujours au décalage 0, aucune entrée de vtable n’est ajoutée.

Retour à auto publication auto

(introduire) n’est pas un choix d’implémentation compliqué, mais cela fait grossir la vtable: la disposition de la vtable est déterminée par le nombre de (introduits) .

Donc, la disposition de la vtable est déterminée par le nombre de fonctions virtuelles (que nous connaissons par la définition de classe), la présence de fonctions virtuelles covariantes (que nous ne pouvons connaître que par les types de retour de fonction) et le type de covariance : covariance primaire, non covariance offset zéro ou covariance virtuelle.

Conclusion

La disposition de la table vtable peut uniquement être déterminée en connaissant le type de retour des surcharges virtuelles des fonctions virtuelles de la classe de base renvoyant un pointeur (ou une référence) vers un type de classe . Le calcul de vtable devrait être retardé quand il y a de tels surcharges dans une classe.

Cela compliquerait la mise en œuvre.

Remarque: les termes tels que “covariance virtuelle” utilisés sont tous constitués, à l’exception de “base primaire” qui est officiellement définie dans l’ABI Itanium C ++.

EDIT: Pourquoi je pense que la vérification de la contrainte n’est pas un problème

La vérification des contraintes covariantes n’est pas un problème, ne rompt pas la compilation séparée, ni le modèle C ++:

écraseur auto d’un pointeur de classe (/ ref) renvoyant une fonction

 struct B { virtual int f(); virtual B *g(); }; struct D : B { auto f(); // int f() auto g(); // ? }; 

Le type de f() est complètement contraint et la définition de la fonction doit renvoyer un int .

Le type de retour de g() est partiellement contraint: il peut s’agir de B* ou de certains derived_from_B* . La vérification aura lieu au sharepoint définition.

Remplacement d’une fonction virtuelle automatique

Considérons une classe dérivée potentielle D2 :

 struct D2 : D { T1 f(); // T1 must be int T2 g(); // ? }; 

Ici, les contraintes sur f() pourraient être vérifiées, car T1 doit être int , mais pas les contraintes sur T2 , car la déclaration de D::g() n’est pas connue. Tout ce que nous soaps, c’est que T2 doit être un pointeur sur une sous-classe de B (éventuellement juste B ).

La définition de D::g() peut être covariante et introduire une contrainte plus forte:

 auto D::g() { return new D; } // covariant D* return 

T2 doit donc être un pointeur sur une classe dérivée de D (éventuellement juste D ).

Avant de voir la définition, nous ne pouvons pas connaître cette contrainte.

Étant donné que la déclaration de substitution ne peut pas être vérifiée avant de voir la définition, elle doit être rejetée .

Pour simplifier, je pense que f() devrait aussi être rejeté.