Utiliser des classes d'amis pour encapsuler des fonctions de membre privé en C ++ - bonne pratique ou abus?

12

J'ai donc remarqué qu'il est possible d'éviter de mettre des fonctions privées dans les en-têtes en faisant quelque chose comme ceci:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        friend class PredicateList_HelperFunctions;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList_HelperFunctions
    {
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList_HelperFunctions::fullMatch(*this);
    }

La fonction privée n'est jamais déclarée dans l'en-tête, et les consommateurs de la classe qui importent l'en-tête n'ont jamais besoin de savoir qu'elle existe. Cela est nécessaire si la fonction d'assistance est un modèle (l'alternative met le code complet dans l'en-tête), c'est ainsi que j'ai "découvert" cela. Un autre avantage de ne pas avoir à recompiler chaque fichier qui inclut l'en-tête si vous ajoutez / supprimez / modifiez une fonction de membre privé. Toutes les fonctions privées se trouvent dans le fichier .cpp.

Donc...

  1. Est-ce un modèle de conception bien connu pour lequel il y a un nom?
  2. Pour moi (venant d'un arrière-plan Java / C # et apprenant le C ++ à mon rythme), cela semble être une très bonne chose, car l'en-tête définit une interface, tandis que le .cpp définit une implémentation (et le temps de compilation amélioré est un joli bonus). Cependant, cela sent aussi qu'il abuse d'une fonctionnalité de langue qui n'est pas destinée à être utilisée de cette façon. Alors, c'est quoi? Est-ce quelque chose que vous fronceriez les sourcils dans un projet C ++ professionnel?
  3. Des pièges auxquels je ne pense pas?

Je connais Pimpl, qui est un moyen beaucoup plus robuste de masquer l'implémentation au bord de la bibliothèque. C'est plus pour une utilisation avec des classes internes, où Pimpl entraînerait des problèmes de performances, ou ne fonctionnerait pas car la classe doit être traitée comme une valeur.


EDIT 2: L'excellente réponse de Dragon Energy ci-dessous suggère la solution suivante, qui n'utilise pas du friendtout le mot - clé:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        class Private;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList::Private
    {
    public:
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList::Private::fullMatch(*this);
    }

Cela évite le facteur de choc friend(qui semble avoir été diabolisé comme goto) tout en conservant le même principe de séparation.

Robert Fraser
la source
2
« Un consommateur pourrait définir sa propre classe PredicateList_HelperFunctions et le laisser accéder aux champs privés. » Ne serait-ce pas une violation ODR ? Vous et le consommateur devez définir la même classe. Si ces définitions ne sont pas égales, alors le code est mal formé.
Nicol Bolas

Réponses:

13

C'est un peu ésotérique pour le moins, comme vous l'avez déjà reconnu, ce qui pourrait me faire me gratter la tête un instant lorsque je commence à rencontrer votre code en me demandant ce que vous faites et où ces classes d'assistance sont implémentées jusqu'à ce que je commence à choisir votre style / habitudes (à quel point je pourrais m'y habituer totalement).

J'aime que vous réduisiez la quantité d'informations dans les en-têtes. Surtout dans les très grandes bases de code, cela peut avoir des effets pratiques pour réduire les dépendances au moment de la compilation et, finalement, les temps de génération.

Ma réaction instinctive est que si vous ressentez le besoin de masquer les détails de l'implémentation de cette manière, pour favoriser le passage des paramètres aux fonctions autonomes avec liaison interne dans le fichier source. Habituellement, vous pouvez implémenter des fonctions utilitaires (ou des classes entières) utiles pour implémenter une classe particulière sans avoir accès à tous les internes de la classe et simplement passer les fonctions pertinentes de l'implémentation d'une méthode à la fonction (ou constructeur). Et naturellement cela a l'avantage de réduire le couplage entre votre classe et les "aides". Il a également tendance à généraliser davantage ce qui aurait pu autrement être des "aides" si vous constatez qu'ils commencent à servir un objectif plus généralisé applicable à plusieurs implémentations de classe.

Il m'arrive aussi parfois de grincer des dents quand je vois beaucoup "d'aides" dans le code. Ce n'est pas toujours vrai, mais parfois ils peuvent être symptomatiques d'un développeur qui décompose les fonctions au gré des volonté pour éliminer la duplication de code avec d'énormes taches de données transmises à des fonctions avec des noms / objectifs à peine compréhensibles au-delà du fait qu'elles réduisent la quantité de code requis pour implémenter d'autres fonctions. Un peu plus de réflexion à l'avance peut parfois conduire à une plus grande clarté sur la façon dont l'implémentation d'une classe est décomposée en fonctions supplémentaires, et favoriser la transmission de paramètres spécifiques à la transmission d'instances entières de votre objet avec un accès complet aux internes peut aider promouvoir ce style de pensée de conception. Je ne dis pas que vous faites ça, bien sûr (je n'en ai aucune idée),

Si cela devient compliqué, j'envisagerais une deuxième solution plus idiomatique qui est le bouton (je me rends compte que vous en avez parlé, mais je pense que vous pouvez généraliser une solution pour éviter celles avec un effort minimal). Cela peut déplacer beaucoup d'informations que votre classe doit implémenter, y compris ses données privées, loin du gros de l'en-tête. Les problèmes de performances du pimpl peuvent être largement atténués avec un allocateur à temps constant bon marché * comme une liste gratuite tout en préservant la sémantique des valeurs sans avoir à implémenter un ctor de copie défini par l'utilisateur à part entière.

  • Pour l'aspect performance, le bouton présente un minimum de surcharge de pointeur, mais je pense que les cas doivent être assez sérieux lorsque cela pose un problème pratique. Si la localité spatiale n'est pas dégradée de manière significative via l'allocateur, vos boucles serrées itérant sur l'objet (qui devraient généralement être homogènes si les performances sont très préoccupantes) auront toujours tendance à minimiser les erreurs de cache dans la pratique à condition d'utiliser quelque chose comme une liste gratuite pour allouer le bouton, plaçant les champs de la classe dans des blocs de mémoire largement contigus.

Personnellement, ce n'est qu'après avoir épuisé ces possibilités que j'envisagerais quelque chose comme ça. Je pense que c'est une idée décente si l'alternative est comme des méthodes plus privées exposées à l'en-tête avec peut-être seulement la nature ésotérique qui est la préoccupation pratique.

Une alternative

Une alternative qui m'est venue à l'esprit tout à l'heure et qui accomplit en grande partie les mêmes objectifs que vos amis absents est la suivante:

struct PredicateListData
{
     int somePrivateField;
};

class PredicateList
{
    PredicateListData data;
public:
    bool match() const;
};

// In source file:
static bool fullMatch(const PredicateListData& p)
{
     // Can access p.somePrivateField here.
}

bool PredicateList::match() const
{
     return fullMatch(data);
}

Maintenant, cela peut sembler être une différence très théorique et je l'appellerais toujours un "assistant" (dans un sens peut-être désobligeant puisque nous passons toujours tout l'état interne de la classe à la fonction, qu'elle en ait besoin ou non) sauf qu'il évite le facteur "choc" de la rencontre friend. En général, cela friendsemble un peu effrayant de voir fréquemment une inspection plus approfondie, car cela dit que vos internes de classe sont accessibles ailleurs (ce qui implique qu'il pourrait être incapable de maintenir ses propres invariants). Avec la façon dont vous l'utilisez, friendcela devient plutôt théorique si les gens sont conscients de lafriendréside simplement dans le même fichier source, aidant à implémenter la fonctionnalité privée de la classe, mais ce qui précède produit à peu près le même effet au moins avec l'avantage peut-être défendable qu'il n'implique aucun ami, ce qui évite tout ce type ("Oh tirer, cette classe a un ami. Où ses soldats sont-ils accessibles / mutés? "). Alors que la version immédiatement ci-dessus communique immédiatement qu'il n'y a aucun moyen pour les utilisateurs privés d'accéder / de muter en dehors de tout ce qui est fait dans la mise en œuvre de PredicateList.

Cela évolue peut-être vers des territoires quelque peu dogmatiques avec ce niveau de nuance, car n'importe qui peut rapidement déterminer si vous nommez uniformément les choses *Helper*et les mettez toutes dans le même fichier source qu'il est en quelque sorte regroupé dans le cadre de l'implémentation privée d'une classe. Mais si nous devenons pointilleux, alors le style immédiatement ci-dessus ne provoquera pas autant de réactions instinctives en un coup d'œil en l'absence du friendmot - clé qui a tendance à sembler un peu effrayant.

Pour les autres questions:

Un consommateur peut définir sa propre classe PredicateList_HelperFunctions et lui permettre d'accéder aux champs privés. Bien que je ne considère pas cela comme un énorme problème (si vous vouliez vraiment dans ces domaines privés, vous pourriez faire du casting), peut-être que cela encouragerait les consommateurs à l'utiliser de cette façon?

Cela pourrait être une possibilité au-delà des limites de l'API où le client pourrait définir une deuxième classe avec le même nom et accéder aux internes de cette façon sans erreurs de liaison. Là encore, je suis en grande partie un codeur C travaillant dans les graphiques où les problèmes de sécurité à ce niveau de "et si" sont très faibles sur la liste des priorités, donc des préoccupations comme celles-ci ne sont que celles auxquelles j'ai tendance à agiter les mains et à faire une danse et essayez de faire comme s'ils n'existaient pas. :-D Si vous travaillez dans un domaine où de telles préoccupations sont plutôt sérieuses, je pense que c'est une considération décente à prendre.

La proposition alternative ci-dessus évite également de souffrir de ce problème. Si vous souhaitez toujours vous en tenir à l'utilisation friend, vous pouvez également éviter ce problème en faisant de l'assistant une classe imbriquée privée.

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

// In source file:
class PredicateList::Helper
{
    ...
};

Est-ce un modèle de conception bien connu pour lequel il y a un nom?

Aucun à ma connaissance. Je doute qu'il y en ait un car il s'agit vraiment de la minutie des détails et du style de mise en œuvre.

"Helper Hell"

J'ai reçu une demande d'éclaircissements sur le point sur la façon dont je grince parfois des dents lorsque je vois des implémentations avec beaucoup de code "d'aide", et cela peut être légèrement controversé avec certains, mais c'est en fait factuel car j'ai vraiment grincé des dents lorsque je déboguais certains de la mise en œuvre d'une classe par mes collègues pour trouver des tas d '"aides". :-D Et je n'étais pas le seul de l'équipe à me gratter la tête en essayant de comprendre ce que tous ces assistants sont censés faire exactement. Je ne veux pas non plus me montrer dogmatique comme "Tu n'utiliseras pas d'aide", mais je ferais une petite suggestion qu'il pourrait aider à réfléchir à la façon de mettre en œuvre des choses absentes quand cela est pratique.

Les fonctions membres privées ne sont-elles pas toutes des fonctions auxiliaires par définition?

Et oui, j'inclus des méthodes privées. Si je vois une classe avec comme une interface publique simple mais comme un ensemble sans fin de méthodes privées qui sont quelque peu mal définies dans le but comme find_implou find_detailou find_helper, alors je grince des dents d'une manière similaire.

Ce que je suggère comme alternative, ce sont des fonctions non membres non amis avec un lien interne (déclaré staticou à l'intérieur d'un espace de noms anonyme) pour aider à implémenter votre classe avec au moins un objectif plus général que "une fonction qui aide à implémenter les autres". Et je peux citer Herb Sutter de C ++ 'Coding Standards' ici pour savoir pourquoi cela peut être préférable d'un point de vue général de SE:

Évitez les frais d'adhésion: dans la mesure du possible, préférez rendre les fonctions non membres non amis. [...] Les fonctions non amis non membres améliorent l'encapsulation en minimisant les dépendances: le corps de la fonction ne peut pas dépendre des membres non publics de la classe (voir le point 11). Ils séparent également les classes monolithiques pour libérer la fonctionnalité séparable, réduisant encore le couplage (voir point 33).

Vous pouvez également comprendre les «frais d'adhésion» dont il parle dans une certaine mesure en termes de principe de base de rétrécissement de la portée variable. Si vous imaginez, comme l'exemple le plus extrême, un objet Dieu qui a tout le code requis pour que votre programme entier s'exécute, alors privilégiez les "assistants" de ce type (fonctions, qu'il s'agisse de fonctions membres ou d'amis) qui peuvent accéder à tous les éléments internes ( privés) d'une classe rendent fondamentalement ces variables non moins problématiques que les variables globales. Vous avez toutes les difficultés à gérer correctement l'état et la sécurité des threads et à maintenir les invariants que vous obtiendriez avec des variables globales dans cet exemple le plus extrême. Et bien sûr, la plupart des exemples réels ne sont, espérons-le, pas proches de cet extrême, mais la dissimulation d'informations n'est utile que car elle limite la portée des informations consultées.

Maintenant, Sutter donne déjà une belle explication ici, mais j'ajouterais également que le découplage a tendance à favoriser comme une amélioration psychologique (du moins si votre cerveau fonctionne comme le mien) en termes de conception des fonctions. Lorsque vous commencez à concevoir des fonctions qui ne peuvent pas accéder à tout dans la classe, sauf uniquement les paramètres pertinents que vous lui transmettez ou, si vous passez l'instance de la classe en tant que paramètre, uniquement ses membres publics, cela tend à promouvoir un état d'esprit de conception qui favorise des fonctions qui ont un objectif plus clair, en plus du découplage et de la promotion d'une encapsulation améliorée, que ce que vous pourriez autrement être tenté de concevoir si vous pouviez simplement accéder à tout.

Si nous revenons aux extrémités, une base de code criblée de variables globales ne tente pas exactement les développeurs de concevoir des fonctions d'une manière claire et généralisée. Très rapidement, plus vous pouvez accéder à des informations dans une fonction, plus nous sommes nombreux, les mortels, à être tentés de la dégénérer et de réduire sa clarté au profit de l'accès à toutes ces informations supplémentaires que nous avons au lieu d'accepter des paramètres plus spécifiques et pertinents pour cette fonction. restreindre son accès à l'État et élargir son applicabilité et améliorer la clarté de ses intentions. Cela s'applique (bien que généralement dans une moindre mesure) aux fonctions des membres ou aux amis.

Dragon Energy
la source
1
Merci pour la contribution! Je ne comprends pas totalement d'où vous venez avec cette partie, cependant: "Je grince parfois un peu quand je vois beaucoup" d'aides "dans le code." - Les fonctions membres privées ne sont-elles pas toutes des fonctions auxiliaires par définition? Cela semble poser problème avec les fonctions de membre privé en général.
Robert Fraser
1
Ah, la classe intérieure n'a pas du tout besoin d'un "ami", donc le faire de cette façon évite totalement le mot
Robert Fraser
"Les fonctions de membre privé ne sont-elles pas toutes des fonctions auxiliaires par définition? Cela semble poser problème avec les fonctions de membre privé en général." Ce n'est pas la chose la plus importante. J'avais l'habitude de penser que c'était une nécessité pratique que pour une implémentation de classe non triviale, vous ayez un certain nombre de fonctions privées ou d'aides avec accès à tous les membres de la classe à la fois. Mais j'ai regardé le style de certains des grands comme Linus Torvalds, John Carmack, et bien que les anciens codes en C, quand il code l'équivalent analogique d'un objet, il parvienne généralement à le coder avec ni avec Carmack.
Dragon Energy
Et naturellement, je pense que les assistants dans le fichier source sont préférables à un en-tête massif qui comprend beaucoup plus d'en-têtes externes que nécessaire car il a utilisé beaucoup de fonctions privées pour aider à implémenter la classe. Mais après avoir étudié le style de ceux ci-dessus et d'autres, j'ai réalisé qu'il était souvent possible d'écrire des fonctions qui sont un peu plus généralisées que les types qui ont besoin d'accéder à tous les membres internes d'une classe même pour implémenter une seule classe, et la pensée à l'avance bien nommer la fonction et lui passer les membres spécifiques dont elle a besoin pour travailler finit par gagner plus de temps [...]
Dragon Energy
[...] qu'il n'en faut, ce qui donne une implémentation plus claire dans l'ensemble qui est plus facile à manipuler plus tard. C'est comme au lieu d'écrire un "prédicat d'assistance" pour "correspondance complète" qui accède à tout dans votre PredicateList, souvent il peut être possible de simplement passer un membre ou deux de la liste des prédicats à une fonction légèrement plus généralisée qui n'a pas besoin d'accéder à chaque membre privé de PredicateList, et souvent cela tendra également à donner un nom et un but plus clairs et plus généralisés à cette fonction interne ainsi que plus de possibilités de «réutilisation rétrospective du code».
Dragon Energy