Pourquoi mettons-nous des fonctions de membre privé dans les en-têtes?

16

La réponse à la raison pour laquelle nous plaçons des variables de membre privé dans les en-têtes C ++ est que la taille de la classe doit être connue aux points où les instances sont déclarées afin que le compilateur puisse générer du code qui se déplace correctement dans la pile.

Pourquoi devons-nous mettre des membres privés dans les en-têtes?

Mais y a-t-il une raison de déclarer des fonctions privées dans la définition de classe?

L'alternative serait essentiellement l'idiome pimpl mais sans l'indirection superflue.

Cette fonctionnalité linguistique est-elle plus qu'une erreur historique?

Praxéolitique
la source

Réponses:

11

Les fonctions membres privées peuvent être virtual, et dans les implémentations courantes de C ++ (qui utilisent une table virtuelle), l'ordre et le nombre spécifiques de fonctions virtuelles doivent être connus de tous les clients de la classe. Cela s'applique même si une ou plusieurs des fonctions membres virtuelles le sont private.

Il peut sembler que cela revient à "mettre le chariot devant le cheval", car les choix d'implémentation du compilateur ne devraient pas affecter la spécification du langage. Cependant, en réalité, le langage C ++ lui-même a été développé en même temps qu'une implémentation fonctionnelle ( Cfront ), qui utilisait vtables.

Greg Hewgill
la source
4
"les choix d'implémentation ne devraient pas affecter le langage" est presque exactement l'opposé du mantra C ++. La spécification ne requiert aucune implémentation particulière, mais il existe de nombreuses règles spécialement conçues pour répondre aux exigences d'un choix d'implémentation particulièrement efficace.
Ben Voigt
Cela semble très plausible. Y a-t-il une source à ce sujet?
Praxéolitique
@Praxeolitic: vous pouvez en savoir plus sur la table des méthodes virtuelles .
Greg Hewgill,
1
@Praxeolitic - trouvez une ancienne copie du 'The Annotated C ++ Refernce Manual' ( stroustrup.com/arm.html ) - les auteurs parlent de divers détails d'implémentation et de la manière dont ils ont façonné le langage.
Michael Kohne
Je ne suis pas sûr que cela réponde vraiment à la question. Pour moi, il ne traite que d'une facette spécifique - quoique bien - et laisse le reste: pourquoi devons-nous mettre des fonctions de membre privé non virtuel dans les en-têtes? Bien sûr, purement pour la cohérence / simplicité est un argument - mais idéalement, nous aurions aussi une justification mécanique et / ou philosophique. J'ai donc ajouté une réponse qui, je crois, explique cela comme un choix de conception très délibéré avec des effets très bénéfiques.
underscore_d
13

Si vous autorisez l'ajout de méthodes à une classe en dehors de sa définition, elles peuvent être ajoutées n'importe où , dans n'importe quel fichier, par n'importe qui.

Cela donnerait immédiatement à tous les codes clients un accès trivial aux membres de données privés et protégés.

Une fois que vous avez terminé la définition de la classe, il n'y a aucun moyen de marquer certains fichiers comme étant spécialement bénis par l'auteur pour l'étendre - il n'y a que des unités de traduction plates. Ainsi, la seule façon raisonnable de dire au compilateur qu'un ensemble particulier de méthodes est officiel, ou béni par l'auteur de la classe, est de les déclarer à l'intérieur de la classe.


Notez que nous avons un accès direct à la mémoire en C ++, ce qui signifie qu'il est généralement trivial de créer un type d'ombre avec la même disposition de mémoire que votre classe, d'ajouter mes propres méthodes (ou simplement de rendre toutes les données publiques), et reinterpret_cast. Ou je peux trouver le code de votre fonction privée, ou le démonter. Ou recherchez l'adresse de la fonction dans la table des symboles et appelez ou directement.

Ces spécificateurs d'accès n'essaient pas d'empêcher ces attaques, car cela n'est pas possible. Ils indiquent uniquement comment une classe est censée être utilisée.

Inutile
la source
1
La définition de classe pourrait faire référence à une entité de conteneur de fonction cachée du code client. Alternativement, cela pourrait être inversé et une définition de classe traditionnelle complète cachée du code client pourrait désigner une interface visible qui ne décrit que les membres publics et peut révéler sa taille.
Praxeolitic
1
Bien sûr, mais le modèle de compilation ne fournit pas un moyen raisonnable d'empêcher le code client d'interposer sa propre version du conteneur caché ou une interface visible différente avec des accesseurs supplémentaires.
Inutile
C'est un bon point, mais n'est-ce pas vrai pour les définitions de toutes les fonctions membres qui ne sont pas dans l'en-tête de toute façon?
Praxéolitique
1
Cependant, toutes les fonctions membres sont déclarées dans la classe. Le fait est que vous ne pouvez pas ajouter de nouvelles fonctions membres qui n'étaient pas au moins déclarées dans la définition de classe (et la règle d'une définition empêche plusieurs définitions).
Inutile
Qu'en est-il alors d'un seul conteneur de règles de fonctions privées?
Praxéolitique
8

La réponse acceptée explique cela pour les fonctions privées virtuelles , mais cela ne répond qu'à une facette spécifique de la question, qui est considérablement plus limitée que ce que le PO a demandé. Donc, nous devons reformuler: Pourquoi sommes-nous tenus de déclarer des fonctions privées non virtuelles dans les en-têtes?

Une autre réponse invoque le fait que les classes doivent être déclarées dans un bloc - après quoi elles sont scellées et ne peuvent pas être ajoutées. C'est ce que vous feriez en omettant de déclarer une méthode privée dans l'en-tête puis en essayant de la définir ailleurs. Joli point. Pourquoi certains utilisateurs de la classe devraient-ils pouvoir l'augmenter d'une manière que d'autres utilisateurs ne peuvent pas observer? Les méthodes privées en font partie et n'en sont pas exclues. Mais ensuite vous demandez pourquoi ils sont inclus, et cela semble un peu tautologique. Pourquoi les utilisateurs de classe doivent-ils les connaître? S'ils n'étaient pas visibles, les utilisateurs ne pouvaient pas en ajouter, et hé hop.

J'ai donc voulu apporter une réponse qui, plutôt que de simplement inclure des méthodes privées par défaut, apporte des points spécifiques en faveur de leur visibilité pour les utilisateurs. Une raison mécanique pour les fonctions privées non virtuelles nécessitant une déclaration publique est donnée dans Herb Sutter's GotW # 100 sur l'idiome Pimpl dans le cadre de sa justification. Je ne parlerai pas de Pimpl ici, car je suis sûr que nous le savons tous. Mais voici le morceau pertinent:

En C ++, lorsque quoi que ce soit dans une définition de classe de fichier d'en-tête change, tous les utilisateurs de cette classe doivent être recompilés - même si la seule modification concerne les membres de classe privés auxquels les utilisateurs de la classe ne peuvent même pas accéder. En effet, le modèle de génération de C ++ est basé sur l'inclusion textuelle et parce que C ++ suppose que les appelants connaissent deux choses principales concernant une classe qui peut être affectée par des membres privés:

  • Taille et disposition : [des membres et des fonctions virtuelles - explicites et parfaits pour la performance, mais pas pourquoi nous sommes ici]
  • Fonctions : Le code appelant doit être capable de résoudre les appels aux fonctions membres de la classe, y compris les fonctions privées inaccessibles qui surchargent de fonctions non privées - si la fonction privée est une meilleure correspondance, le code appelant ne pourra pas être compilé. (C ++ a pris la décision délibérée de concevoir une résolution de surcharge avant de vérifier l'accessibilité pour des raisons de sécurité. Par exemple, il a été estimé que le fait de changer l'accessibilité d'une fonction du privé au public ne devrait pas changer la signification du code d'appel légal.)

Sutter est, bien sûr, une source extrêmement fiable en tant que membre du Comité, donc il connaît "une décision délibérée de conception" quand il en voit une. Et l'idée d'exiger la déclaration publique de méthodes privées comme moyen d'éviter une sémantique altérée ou une accessibilité accidentellement rompue plus tard est probablement la justification la plus convaincante. Heureusement, car le tout semblait plutôt inutile avant maintenant!

underscore_d
la source
" Mais alors vous demandez pourquoi, et cette réponse semble tautologique. Encore une fois, pourquoi les utilisateurs de la classe ont-ils besoin de les connaître? " Non, ce n'est pas tautologique. Les utilisateurs n'ont pas nécessairement "besoin de les connaître". Ce qui doit arriver, c'est que vous devez être en mesure d'empêcher les gens de prolonger les cours sans le consentement de l'auteur de la classe. Tous ces membres doivent donc être déclarés d’emblée. Le fait que les utilisateurs connaissent les membres privés n'est qu'une conséquence nécessaire.
Nicol Bolas
1
@NicolBolas Bien sûr. Ce n'était probablement pas un très bon libellé de ma part. Je voulais dire que la réponse n'explique que la visibilité des méthodes privées en raison d'une règle (très valide) qui couvre beaucoup de choses, plutôt que de fournir une justification de la visibilité des méthodes privées en particulier. Bien sûr, la vraie réponse est une combinaison d'Useless, de Gnasher et de la mienne. Je veux juste fournir une autre perspective qui n'était pas encore là.
underscore_d
4

Il y a deux raisons à cela.

Tout d'abord, réalisez que le spécificateur d'accès est destiné au compilateur et n'est pas pertinent au moment de l'exécution. L'accès à un membre privé en dehors de la portée est une erreur de compilation .

Concision

Prenons une fonction courte, une ou deux lignes. Il existe pour réduire la réplication de code ailleurs, ce qui a également l'avantage de pouvoir changer la façon dont un algorithme ou quoi que ce soit d'autre fonctionne à un seul endroit au lieu de plusieurs (par exemple changer un algorithme de tri).

Souhaitez-vous plutôt avoir une ou deux lignes rapides dans l'en-tête, ou avoir le prototype de fonction plus une implémentation quelque part? Il est plus facile à trouver dans l'en-tête, et pour les fonctions courtes, il est beaucoup plus détaillé d'avoir une implémentation distincte.

Il y a un autre avantage majeur, qui est ...

Fonctions en ligne

Une fonction privée peut être en ligne, et cela nécessite nécessairement qu'elle soit dans l'en-tête. Considère ceci:

class A {
  private:
    inline void myPrivateFunction() {
      ...
    }

  public:
    inline void somePublicFunction() {
      myPrivateFunction();
      ...
    }
};

La fonction privée peut être alignée avec la fonction publique. Cela se fait à la discrétion du compilateur, car le inlinemot clé est techniquement une suggestion , pas une exigence.

Communauté
la source
Toutes les fonctions définies à l'intérieur du corps de classe sont automatiquement marquées inline, il n'y a aucune raison d'y utiliser le mot-clé.
Ben Voigt
@BenVoigt il en est ainsi. Je ne m'en rendais pas compte et j'utilise C ++ à temps partiel depuis un bon moment. Cette langue ne cesse de me surprendre avec de petites pépites comme celle-là.
Je ne pense pas qu'une méthode doive être dans l'en-tête pour être intégrée. Il doit juste être dans la même unité de compilation. Y a-t-il un cas où il est logique d'avoir des unités de compilation séparées pour une classe?
Samuel Danielson
@SamuelDanielson correct, mais une fonction privée doit figurer dans la définition de classe. La notion même de «privé» implique qu'il fait partie de la classe. Il serait possible d'avoir une fonction non membre dans le .cppfichier qui est insérée par les fonctions membres qui sont définies en dehors de la définition de classe, mais une telle fonction ne serait pas privée.
Le corps de la question était "y a-t-il une raison de déclarer des fonctions privées dans la définition de classe [sic, par laquelle le contexte montre que OP signifie vraiment une déclaration de classe]". Vous parlez de définir les fonctions privées. Cela ne répond pas à la question. @SamuelDanielson Aujourd'hui, le LTO signifie que les fonctions peuvent être n'importe où dans un projet et conserver une chance égale d'être intégrées. En ce qui concerne le fractionnement d'une classe en plusieurs unités de traduction, le cas le plus simple est que la classe est juste grande et que vous souhaitez la diviser sémantiquement en plusieurs fichiers source. Je suis sûr que de si grandes classes sont découragées, mais de toute façon
underscore_d
2

Une autre raison d'avoir des méthodes privées dans le fichier d'en-tête: Il existe des cas où une méthode en ligne publique ne fait pas beaucoup plus que d'appeler une ou plusieurs méthodes privées. Le fait d'avoir les méthodes privées dans l'en-tête signifie qu'un appel à la méthode publique peut être complètement aligné sur le code réel des méthodes privées, et l'inline ne s'arrête pas avec un appel à la méthode privée. Même à partir d'une unité de compilation différente (et les méthodes publiques généralement appelées à partir de différentes unités de compilation).

Bien sûr, il y a aussi la raison pour laquelle le compilateur ne peut pas détecter les problèmes de résolution de surcharge s'il ne connaît pas toutes les méthodes, y compris les méthodes privées.

gnasher729
la source
Excellent point sur la doublure! J'imagine que cela est particulièrement pertinent pour stdlib et d'autres bibliothèques avec des méthodes définies en ligne. (si ce n'est pas tant pour le code interne, où le LTO peut faire presque n'importe quoi au-delà des limites de TU)
underscore_d
0

C'est pour permettre à ces fonctions d'accéder aux membres privés. Sinon, vous en auriez besoin frienddans l'en-tête de toute façon.

Si une fonction pouvait accéder aux membres privés de la classe, alors private serait inutile.

monstre à cliquet
la source
1
J'aurais peut-être dû être plus explicite dans la question - je voulais dire pourquoi le langage est-il conçu de cette façon? Pourquoi doit-il être ou pourquoi cette conception est-elle bonne?
Praxeolitic