Équivalent en ami C ++ granulaire propre? (Réponse: Idiome client-avocat)

Pourquoi C ++ a-t-il public membres public que n’importe qui peut appeler et des déclarations d’ friend qui exposent tous private membres private à des classes ou méthodes étrangères, mais n’offrent aucune syntaxe pour exposer des membres particuliers à des appelants donnés?

Je veux exprimer des interfaces avec certaines routines à invoquer uniquement par des appelants connus sans avoir à donner à ces appelants un access complet à tous les privés, ce qui semble être une chose raisonnable à vouloir. Le mieux que je puisse trouver (ci-dessous) et les suggestions des autres tournent encore autour des idiomes / motifs indirects variables, où je veux vraiment avoir un moyen d’avoir des définitions de classes simples et simples qui indiquent explicitement ce que les appelants , mes enfants , ou absolument personne ) peuvent accéder à quels membres. Quelle est la meilleure façon d’exprimer le concept ci-dessous?

 // Can I grant Y::usesX(...) selective X::ressortingcted(...) access more cleanly? void Y::usesX(int n, X *x, int m) { X::AttorneyY::ressortingcted(*x, n); } struct X { class AttorneyY; // Proxies ressortingcted state to part or all of Y. private: void ressortingcted(int); // Something preferably selectively available. friend class AttorneyY; // Give trusted member class private access. int personal_; // Truly private state ... }; // Single abstract permission. Can add more friends or forwards. class X::AttorneyY { friend void Y::usesX(int, X *, int); inline static void ressortingcted(X &x, int n) { x.ressortingcted(n); } }; 

Je ne suis pas loin d’être un gourou de l’organisation de logiciels, mais j’ai l’impression que la simplicité des interfaces et le principe du moindre privilège sont directement opposés à cet aspect du langage. Un exemple plus clair de mon désir pourrait être une classe Person avec des méthodes déclarées comme takePill(Medicine *) tellTheTruth() et forfeitDollars(unsigned int) que seules les méthodes / membres de Physician , Judge ou TaxMan , respectivement, devraient même envisager d’invoquer. J’ai besoin de classes proxy ou d’interface ponctuelles pour chaque aspect majeur de l’interface, mais dites-moi si vous me manquez.

Réponse acceptée de Drew Hall : Dr Dobbs – Friendship and the Attorney-Client Idiom

Le code ci-dessus appelait à l’origine la classe wrapper ‘Proxy’ au lieu de ‘Attorney’ et utilisait des pointeurs à la place des références mais était autrement équivalent à ce que Drew a trouvé, que j’ai alors considéré comme la meilleure solution connue. (Ne pas me féliciter trop fort sur le dos …) J’ai aussi changé la signature de ‘ressortingctions’ pour montrer le transfert de parameters. Le coût global de cet idiome est une déclaration de classe et un ami par ensemble d’permissions, une déclaration d’ami par appelant défini par jeu et un wrapper de transfert par méthode exposée par jeu d’permissions. La plus grande partie de la meilleure discussion ci-dessous tourne autour de la question de la transmission des appels selon laquelle un idiome «clé» très similaire évite au désortingment d’une protection moins directe.

L’ idiome client-avocat peut être ce que vous recherchez. Les mécanismes ne sont pas trop différents de votre solution de classe proxy membre, mais cette méthode est plus idiomatique.

Il existe un modèle très simple, qui a été doublé rétroactivement de PassKey , et qui est très simple en C ++ 11 :

 template  class Key { friend T; Key() {} Key(Key const&) {} }; 

Et avec cela:

 class Foo; class Bar { public: void special(int a, Key); }; 

Et le site d’appel, dans toute méthode Foo , ressemble à:

 Bar().special(1, {}); 

Remarque: si vous êtes bloqué en C ++ 03, passez à la fin du message.

Le code est d’une simplicité trompeuse, il intègre quelques points clés qui méritent d’être élaborés.

Le nœud du motif est que:

  • appeler Bar::special nécessite de copier une Key dans le contexte de l’appelant
  • seul Foo peut construire ou copier une Key

Il est à noter que:

  • les classes dérivées de Foo ne peuvent pas construire ou copier Key car l’amitié n’est pas transitive
  • Foo lui-même ne peut pas transmettre une Key à quiconque pour appeler Bar::special car l’appel nécessite non seulement de conserver une instance, mais de faire une copie.

Parce que C ++ est C ++, il y a quelques pièges à éviter:

  • le constructeur de la copie doit être défini par l’utilisateur, sinon il est public par défaut
  • le constructeur par défaut doit être défini par l’utilisateur, sinon il est public par défaut
  • le constructeur par défaut doit être défini manuellement , car = default autoriserait l’initialisation de l’agrégat pour contourner le constructeur par défaut défini par l’utilisateur (et permettre ainsi à tout type d’obtenir une instance)

Ceci est assez subtil pour que, pour une fois, je vous conseille de copier / coller la définition ci-dessus de Key verbatim plutôt que de tenter de la reproduire à partir de la mémoire.


Une variante permettant la délégation:

 class Bar { public: void special(int a, Key const&); }; 

Dans cette variante, toute personne ayant une instance de Key peut appeler Bar::special , alors même si seul Foo peut créer une Key , elle peut alors diffuser les informations d’identification aux lieutenants de confiance.

Dans cette variante, afin d’éviter qu’un lieutenant non autorisé ne divulgue la clé, il est possible de supprimer entièrement le constructeur de copie, ce qui permet d’attacher la durée de vie de la clé à une scope lexicale particulière.


Et en C ++ 03?

Eh bien, l’idée est similaire, sauf que l’ friend T; n’est pas une chose, il faut donc créer un nouveau type de clé pour chaque détenteur:

 class KeyFoo { friend class Foo; KeyFoo () {} KeyFoo (KeyFoo const&) {} }; class Bar { public: void special(int a, KeyFoo); }; 

Le motif est suffisamment répétitif pour que cela puisse valoir une macro pour éviter les fautes de frappe.

L’initialisation globale n’est pas un problème, mais la syntaxe = default n’est pas non plus disponible.


Merci aux personnes qui ont consortingbué à améliorer cette réponse au fil des ans:

  • Luc Touraille , pour m’avoir signalé dans les commentaires de cette class KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} }; class KeyFoo: boost::noncopyable { friend class Foo; KeyFoo() {} }; désactive complètement le constructeur de copie et ne fonctionne donc que dans la variante de délégation (empêchant l’instance de stockage).
  • K-ballo , pour avoir souligné comment C ++ 11 a amélioré la situation avec un friend T;

Vous pouvez utiliser un modèle décrit dans le livre de Jeff Aldger “C ++ for real programmers”. Il n’a pas de nom particulier mais il est appelé «pierres précieuses et facettes». L’idée de base est la suivante: parmi votre classe principale qui contient toute la logique, vous définissez plusieurs interfaces (pas des interfaces réelles, comme elles) qui implémentent des sous-parties de cette logique. Chacune de ces interfaces (facette en termes de livre) donne access à certaines des logiques de la classe principale (pierre gemme). De plus, chaque facette contient le pointeur sur l’instance de pierre précieuse.

Qu’est-ce que cela signifie pour toi?

  1. Vous pouvez utiliser n’importe quelle facette partout au lieu de pierres précieuses.
  2. Les utilisateurs de facettes n’ont pas besoin de connaître la structure des pierres précieuses, car ils pourraient être déclarés et utilisés via le modèle PIMPL.
  3. D’autres classes peuvent se référer à la facette plutôt qu’à la pierre gemme – c’est la réponse à votre question sur la façon d’exposer des méthodes limitées à une classe spécifiée.

J’espère que cela t’aides. Vous souhaitez que je puisse poster ici des exemples de code pour illustrer plus clairement ce schéma.

EDIT: Voici le code:

 class Foo1; // This is all the client knows about Foo1 class PFoo1 { private: Foo1* foo; public: PFoo1(); PFoo1(const PFoo1& pf); ~PFoo(); PFoo1& operator=(const PFoo1& pf); void DoSomething(); void DoSomethingElse(); }; class Foo1 { friend class PFoo1; protected: Foo1(); public: void DoSomething(); void DoSomethingElse(); }; PFoo1::PFoo1() : foo(new Foo1) {} PFoo1::PFoo(const PFoo1& pf) : foo(new Foo1(*(pf {} PFoo1::~PFoo() { delete foo; } PFoo1& PFoo1::operator=(const PFoo1& pf) { if (this != &pf) { delete foo; foo = new Foo1(*(pf.foo)); } return *this; } void PFoo1::DoSomething() { foo->DoSomething(); } void PFoo1::DoSomethingElse() { foo->DoSomethingElse(); } Foo1::Foo1() { } void Foo1::DoSomething() { cout << “Foo::DoSomething()” << endl; } void Foo1::DoSomethingElse() { cout << “Foo::DoSomethingElse()” << endl; } 

EDIT2: Votre classe Foo1 pourrait être plus complexe, par exemple, elle contient deux autres méthodes:

 void Foo1::DoAnotherThing() { cout << “Foo::DoAnotherThing()” << endl; } void Foo1::AndYetAnother() { cout << “Foo::AndYetAnother()” << endl; } 

Et ils sont accessibles via la class PFoo2

 class PFoo2 { private: Foo1* foo; public: PFoo2(); PFoo2(const PFoo1& pf); ~PFoo(); PFoo2& operator=(const PFoo2& pf); void DoAnotherThing(); void AndYetAnother(); }; void PFoo1::DoAnotherThing() { foo->DoAnotherThing(); } void PFoo1::AndYetAnother() { foo->AndYetAnother(); } 

Ces méthodes ne sont pas dans la classe PFoo1 , vous ne pouvez donc pas y accéder par le biais. De cette façon, vous pouvez diviser le comportement de Foo1 en deux (ou plus) facettes PFoo1 et PFoo2. Ces classes de facettes pourraient être utilisées à différents endroits et leur appelant ne devrait pas connaître l'implémentation de Foo1. Peut-être que ce n'est pas ce que vous voulez vraiment, mais ce que vous voulez est impossible pour C ++, et c'est un travail, mais peut-être trop verbeux ...

Je sais que c’est une vieille question, mais le problème est toujours d’actualité. Bien que j’aime l’idée de l’idiome Attorney-Client, je voulais une interface transparente pour les classes de clients bénéficiant d’un access privé (ou protégé).

J’imagine que quelque chose de similaire a déjà été fait, mais un coup d’œil rapide n’a rien donné. La méthode suivante (C ++ 11 up) fonctionne par classe (pas par object) et utilise une classe de base CRTP utilisée par la ‘classe privée’ pour exposer un foncteur public. Seules les classes auxquelles un access spécifique a été donné peuvent appeler l’opérateur () du foncteur, qui appelle alors directement la méthode privée associée via une référence stockée.

Il n’y a pas de surcharge d’appel de fonction et la seule surcharge de mémoire est une référence par méthode privée nécessitant une exposition. Le système est très polyvalent. toute signature de fonction et type de retour est autorisé, de même que l’appel de fonctions virtuelles dans la classe privée.

Pour moi, le principal avantage est celui de la syntaxe. Bien qu’une déclaration certes assez laide des objects foncteurs soit requirejse dans la classe privée, celle-ci est complètement transparente pour les classes clientes. Voici un exemple tiré de la question initiale:

 struct Doctor; struct Judge; struct TaxMan; struct TheState; struct Medicine {} meds; class Person : private GranularPrivacy { private: int32_t money_; void _takePill (Medicine *meds) {std::cout << "yum..."< ::Function <&Person::_takePill> ::Allow  takePill; Signature  ::Function <&Person::_tellTruth> ::Allow  tellTruth; Signature  ::Function <&Person::_payDollars> ::Allow  payDollars; }; struct Doctor { Doctor (Person &patient) { patient.takePill(&meds); // std::cout << patient.tellTruth(); //Not allowed } }; struct Judge { Judge (Person &defendant) { // defendant.payDollars (20); //Not allowed std::cout << defendant.tellTruth() < 

La classe de base GranularPrivacy fonctionne en définissant 3 classes de modèles nestedes. Le premier, «Signature», prend le type de retour de fonction et la signature de fonction comme parameters de modèle et les transmet à la fois à la méthode operator () du functor et à la deuxième classe de modèle d’imbrication, «Function». Ceci est paramétré par un pointeur vers une fonction membre privée de la classe Host, qui doit avoir la signature fournie par la classe Signature. En pratique, deux classes 'Fonction' distinctes sont utilisées; l'un donné ici, et l'autre pour les fonctions const, omis pour des raisons de concision.

Enfin, la classe Allow hérite récursivement d'une classe de base instanciée explicitement à l'aide du mécanisme de template variadic, en fonction du nombre de classes spécifié dans la liste d'arguments de son template. Chaque niveau d'inheritance de Allow a un ami de la liste de modèles et les instructions using apportent le constructeur et l'opérateur operator de la classe de base dans la hiérarchie d'inheritance.

 template  class GranularPrivacy { friend Host; template  class Signature { friend Host; typedef ReturnType (Host::*FunctionPtr) (Args... args); template  class Function { friend Host; template  class Allow { Host &host_; protected: Allow (Host &host) : host_ (host) {} ReturnType operator () (Args... args) {return (host_.*function)(args...);} }; template  class Allow  : public Allow  { friend Friend; friend Host; protected: using Allow ::Allow; using Allow ::operator (); }; }; }; }; 

J'espère que quelqu'un trouve cela utile, tout commentaire ou suggestion serait le bienvenu. Ceci est certainement toujours en cours - je voudrais en particulier fusionner les classes Signature et Function en une seule classe de template, mais j'ai eu du mal à trouver un moyen de le faire. Des exemples plus complets, exécutables, sont disponibles sur cpp.sh/6ev45 et cpp.sh/2rtrj .

Quelque chose qui s’apparente au code ci-dessous vous permettra de contrôler avec précision les parties de votre état privé que vous rendez public via le mot-clé friend .

 class X { class SomewhatPrivate { friend class YProxy1; void ressortingcted(); }; public: ... SomewhatPrivate &get_somewhat_private_parts() { return priv_; } private: int n_; SomewhatPrivate priv_; }; 

MAIS:

  1. Je ne pense pas que ça en vaille la peine.
  2. La nécessité d’utiliser le mot-clé friend peut suggérer que votre conception est imparfaite, peut-être y a-t-il un moyen de faire ce dont vous avez besoin sans elle. J’essaie de l’éviter mais si cela rend le code plus lisible, maintenable ou réduit le besoin de code passe-partout, je l’utilise.

EDIT: Pour moi, le code ci-dessus est (généralement) une abomination qui devrait (généralement) ne pas être utilisée.

J’ai écrit une amélioration mineure à la solution proposée par Matthieu M. La limitation de sa solution est que vous ne pouvez accorder l’access qu’à une seule classe. Que faire si je veux laisser l’une des trois classes y accéder?

 #include  #include  struct force_non_aggregate {}; template struct ressortingct_access_to : private force_non_aggregate { template, std::decay_t>{})>::type> constexpr ressortingct_access_to(ressortingct_access_to) noexcept {} ressortingct_access_to() = delete; ressortingct_access_to(ressortingct_access_to const &) = delete; ressortingct_access_to(ressortingct_access_to &&) = delete; }; template struct access_requester; template struct ressortingct_access_to : private force_non_aggregate { private: friend T; friend access_requester; ressortingct_access_to() = default; ressortingct_access_to(ressortingct_access_to const &) = default; ressortingct_access_to(ressortingct_access_to &&) = default; }; // This intermediate class gives us nice names for both sides of the access template struct access_requester { static constexpr auto request_access_as = ressortingct_access_to{}; }; template constexpr auto const & request_access_as = access_requester::request_access_as; struct S; struct T; auto f(ressortingct_access_to) {} auto g(ressortingct_access_to x) { static_cast(x); // f(x); // Does not comstack } struct S { S() { g(request_access_as); g({}); f(request_access_as); // f(request_access_as); // Does not comstack // f({request_access_as}); // Does not comstack } }; struct T { T() { f({request_access_as}); // g({request_access_as}); // Does not comstack // g({}); // Does not comstack } }; 

Cela utilise une approche légèrement différente pour rendre l’object pas un agrégat. Plutôt que d’avoir un constructeur fourni par l’utilisateur, nous avons une classe de base privée vide. En pratique, cela n’a probablement pas d’importance, mais cela signifie que cette implémentation est une classe POD car elle rest sortingviale. L’effet doit restr le même, cependant, car personne ne va stocker ces objects de toute façon.