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...
- Est-ce un modèle de conception bien connu pour lequel il y a un nom?
- 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?
- 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 friend
tout 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.
la source
Réponses:
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.
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.
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:
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, celafriend
semble 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,friend
cela devient plutôt théorique si les gens sont conscients de lafriend
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 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 dePredicateList
.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 dufriend
mot - clé qui a tendance à sembler un peu effrayant.Pour les autres questions:
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.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.
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_impl
oufind_detail
oufind_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é
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: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.
la source
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é dePredicateList
, 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».