web-dev-qa-db-fra.com

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

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. Ceci 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 dans la bibliothèque Edge. 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 tout le mot clé friend:

// 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 de friend (qui semble avoir été diabolisé comme goto) tout en conservant le même principe de séparation.

12
Robert Fraser

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'aide 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 façon, 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 "assistants" 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 de manière volontaire 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é en ce qui concerne 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 suggère pas que vous fassiez cela, bien sûr (je n'en ai aucune idée), mais peut-être pour faire attention à cette tendance lorsque nous sommes trop satisfaits des "aides".

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 de l'en-tête en gros. 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 au moins un surcoût 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, en plaçant les champs de la classe dans des blocs de mémoire largement contigus.

Personnellement, c'est seulement après avoir épuisé ces possibilités que je considérerais 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 étant la préoccupation pratique.

ne 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 transmettons 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 rencontrer friend. En général, friend semble un peu effrayant de voir fréquemment une inspection plus approfondie, car il dit que les internes de votre classe sont accessibles ailleurs (ce qui implique qu'il pourrait être incapable de maintenir ses propres invariants). Avec la façon dont vous utilisez friend, cela devient plutôt théorique si les gens sont conscients de la pratique puisque le friend réside simplement dans le même fichier source, aidant à implémenter la fonctionnalité privée de la classe, mais ce qui précède accomplit à 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 ce genre de chose ("Oh shoot, cette classe a un ami. Où d'autre ses soldats sont-ils accédés/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 l'implémentation 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 mettre tous 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 causera pas autant de réaction instinctive en un coup d'œil en l'absence du mot-clé friend 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 voulez toujours vous en tenir à l'utilisation de 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 quand je vois des implémentations avec beaucoup de code "d'aide", et cela pourrait être légèrement controversé avec certains mais c'est en fait factuel car j'ai vraiment grincé des dents quand 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 que cela pourrait aider à penser sur la façon de mettre en œuvre des choses absentes d'eux lorsque cela est possible.

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 infini de méthodes privées qui sont quelque peu mal définies comme find_impl ou find_detail ou find_helper, puis je grimace aussi d'une manière similaire.

Ce que je suggère comme alternative, ce sont des fonctions non membres non amis avec une liaison interne (déclarée static ou à 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 du principe de base de la réduction 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 tout votre programme s'exécute, alors privilégiez les "aides" 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 est seulement aussi utile qu'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 dont nous disposons au lieu d'accepter des paramètres plus spécifiques et pertinents pour cette fonction. restreindre son accès à l'État, é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.

13
Dragon Energy