Pourquoi le C ++ n'autorise-t-il pas l'amitié héritée?

93

Pourquoi l'amitié n'est-elle pas au moins éventuellement héritable en C ++? Je comprends que la transitivité et la réflexivité sont interdites pour des raisons évidentes (je dis cela uniquement pour éviter les réponses simples de citation de FAQ), mais l'absence de quelque chose du virtual friend class Foo;genre me laisse perplexe. Quelqu'un connaît-il le contexte historique derrière cette décision? L'amitié était-elle vraiment juste un hack limité qui a depuis trouvé sa place dans quelques utilisations respectables obscures?

Modifier pour clarification: je parle du scénario suivant, pas où les enfants de A sont exposés à B ou à la fois à B et à ses enfants. Je peux également imaginer accorder éventuellement l'accès aux remplacements de fonctions d'amis, etc.

class A {
  int x;
  friend class B;
};

class B {
  // OK as per friend declaration above.
  void foo(A& a, int n) { a.x = n; }
};

class D : public B { /* can't get in A w/o 'friend class D' declaration. */ };

Réponse acceptée: comme le déclare Loki , l'effet peut être simulé plus ou moins en créant des fonctions proxy protégées dans des classes de base amies, il n'est donc pas nécessaire d'accorder une amitié à une classe ou à une méthode virtuelle. Je n'aime pas le besoin de procurations standard (ce que la base amie devient effectivement), mais je suppose que cela a été jugé préférable à un mécanisme de langage qui serait plus probablement mal utilisé la plupart du temps. Je pense qu'il est probablement temps d'acheter et de lire The Design and Evolution of C ++ de Stroupstrup , que j'ai vu assez de gens ici recommander, pour mieux comprendre ce type de questions ...

Jeff
la source

Réponses:

93

Parce que je peux écrire Fooet son ami Bar(il y a donc une relation de confiance).

Mais est-ce que je fais confiance aux personnes qui écrivent des classes dérivées Bar?
Pas vraiment. Ils ne devraient donc pas hériter d'amitié.

Tout changement dans la représentation interne d'une classe nécessitera une modification de tout ce qui dépend de cette représentation. Ainsi, tous les membres d'une classe et tous les amis de la classe devront être modifiés.

Par conséquent, si la représentation interne de Fooest modifiée, elle Bardoit également l'être (car l'amitié est étroitement liée Barà Foo). Si l 'amitié était héritée, alors toute classe dérivée de Barserait également étroitement liée Fooet nécessiterait donc une modification si Foola représentation interne de s est modifiée. Mais je n'ai aucune connaissance des types dérivés (et je ne devrais pas non plus. Ils peuvent même être développés par différentes sociétés, etc.). Ainsi, je ne pourrais pas changer Foocar cela introduirait des changements de rupture dans la base de code (car je ne pourrais pas modifier toutes les classes dérivées Bar).

Ainsi, si l'amitié a été héritée, vous introduisez par inadvertance une restriction sur la capacité de modifier une classe. Cela n'est pas souhaitable car vous rendez fondamentalement inutile le concept d'API publique.

Remarque: un enfant de Barpeut accéder Fooen utilisant Bar, il suffit de rendre la méthode Barprotégée. Ensuite, l'enfant de Barpeut accéder à a Fooen appelant via sa classe parent.

c'est ce que tu veux?

class A
{
    int x;
    friend class B;
};

class B
{
    protected:
       // Now children of B can access foo
       void foo(A& a, int n) { a.x = n; }
};

class D : public B
{
    public:
        foo(A& a, int n)
        {
            B::foo(a, n + 5);
        }
};
Loki Astari
la source
4
Bingo. Il s'agit de limiter les dommages que vous pouvez causer en modifiant les éléments internes d'une classe.
j_random_hacker
Franchement, le cas auquel je pense vraiment est le modèle avocat-client, où un intermédiaire agit comme une interface limitée aux classes extérieures en présentant des méthodes wrapper à la classe d'accès restreint sous-jacente. Dire qu'une interface est disponible pour tous les enfants des autres classes plutôt qu'une classe exacte serait beaucoup plus utile que le système actuel.
Jeff
@Jeff: exposer la représentation interne à tous les enfants d'une classe rendra le code immuable (cela rompt également l'encapsulation car toute personne souhaitant accéder aux membres internes n'a qu'à hériter de Bar même si ce n'est pas vraiment un Bar ).
Martin York
@Martin: Oui, dans ce schéma, la base amicale pourrait être utilisée pour accéder à la classe amie, ce qui pourrait être une simple violation d'encapsulation dans de nombreux cas (sinon la plupart). Cependant, dans les situations où la base amicale est une classe abstraite, toute classe dérivée serait contrainte d'implémenter sa propre interface réciproque. Je ne sais pas si une classe «imposteur» dans ce scénario serait considérée comme rompant l'encapsulation ou violant un contrat d'interface si elle n'essayait pas de jouer fidèlement son rôle revendiqué correctement.
Jeff
@Martin: Oui, c'est l'effet que je veux et que j'utilise parfois déjà, où A est en fait une interface réciproquement amicale avec une classe d'accès restreint Z. La plainte commune avec l'idiome avocat-client normal semble être que la classe d'interface A doit utiliser des wrappers d'appel standard vers Z, et afin d'étendre l'accès aux sous-classes, le passe-partout de A doit être essentiellement dupliqué dans chaque classe de base comme B.Une interface exprime normalement les fonctionnalités qu'un module veut offrir, pas les fonctionnalités des autres veut s'utiliser.
Jeff
48

Pourquoi l'amitié n'est-elle pas au moins éventuellement héritable en C ++?

Je pense que la réponse à votre première question se trouve dans cette question: "Les amis de votre père ont-ils accès à vos particuliers?"

Wilx
la source
36
Pour être honnête, cette question soulève des questions inquiétantes à propos de votre père. . .
iheanyi
3
Quel est l'intérêt de cette réponse? au mieux un commentaire douteux mais peut-être léger
DeveloperChris
11

Une classe amie peut exposer son ami via les fonctions d'accesseur, puis lui accorder l'accès.

class stingy {
    int pennies;
    friend class hot_girl;
};

class hot_girl {
public:
    stingy *bf;

    int &get_cash( stingy &x = *bf ) { return x.pennies; }
};

class moocher {
public: // moocher can access stingy's pennies despite not being a friend
    int &get_cash( hot_girl &x ) { return x.get_cash(); }
};

Cela permet un contrôle plus fin que la transitivité facultative. Par exemple, get_cashpeut être protectedou peut appliquer un protocole d'accès limité à l'exécution.

Potatoswatter
la source
@Hector: Votez pour la refactorisation! ;)
Alexander Shukaev
7

Norme C ++, section 11.4 / 8

L'amitié n'est ni héréditaire ni transitive.

Si l'amitié était héritée, alors une classe qui n'était pas censée être un ami aurait soudainement accès à vos internes de classe et qui viole l'encapsulation.

David
la source
2
Dites Q "amis" A, et B est dérivé de A. Si B hérite de l'amitié de A, alors parce que B est un type de A, techniquement, c'est un A qui a accès aux soldats de Q. Cela ne répond donc pas à la question avec aucune raison pratique.
mdenton8
2

Parce que c'est simplement inutile.

L'utilisation du friendmot-clé est en soi suspecte. En terme de couplage, c'est la pire des relations (bien avant l'héritage et la composition).

Tout changement dans les éléments internes d'une classe risque d'avoir un impact sur les amis de cette classe ... voulez-vous vraiment un nombre inconnu d'amis? Vous ne seriez même pas en mesure de les lister si ceux qui en héritent pouvaient aussi être des amis, et vous courriez le risque de casser le code de vos clients à chaque fois, ce n'est certainement pas souhaitable.

J'avoue volontiers que pour les devoirs / projets pour animaux de compagnie, la dépendance est souvent une considération lointaine. Sur les projets de petite taille, cela n'a pas d'importance. Mais dès que plusieurs personnes travaillent sur le même projet et que cela devient des dizaines de milliers de lignes, il faut limiter l'impact des changements.

Cela apporte une règle très simple:

La modification des éléments internes d'une classe ne devrait affecter que la classe elle-même

Bien sûr, vous affecterez probablement ses amis, mais il y a deux cas ici:

  • fonction libre ami: probablement plus une fonction membre de toute façon (je pense std::ostream& operator<<(...)ici, qui n'est pas membre purement par accident des règles de langue
  • classe d'amis? vous n'avez pas besoin de cours d'amis sur de vraies classes.

Je recommanderais l'utilisation de la méthode simple:

class Example;

class ExampleKey { friend class Example; ExampleKey(); };

class Restricted
{
public:
  void forExampleOnly(int,int,ExampleKey const&);
};

Ce Keymodèle simple vous permet de déclarer un ami (d'une certaine manière) sans lui donner réellement accès à vos internes, l'isolant ainsi des changements. De plus, il permet à cet ami de prêter sa clé aux fiduciaires (comme les enfants) si nécessaire.

Matthieu M.
la source
0

Une supposition: si une classe déclare une autre classe / fonction comme ami, c'est parce que cette deuxième entité a besoin d'un accès privilégié à la première. Quelle est l'utilité d'accorder à la deuxième entité un accès privilégié à un nombre arbitraire de classes dérivées de la première?

Oliver Charlesworth
la source
2
Si une classe A voulait accorder l'amitié à B et à ses descendants, elle pourrait éviter de mettre à jour son interface pour chaque sous-classe ajoutée ou de forcer B à écrire un passe-partout passe-partout, ce qui est la moitié du point d'amitié en premier lieu je pense.
Jeff
@Jeff: Ah, alors j'ai mal compris votre sens. J'ai supposé que vous vouliez dire que vous Bauriez accès à toutes les classes héritées de A...
Oliver Charlesworth
0

Une classe dérivée ne peut hériter que de quelque chose, qui est «membre» de la base. Une déclaration d'ami ne fait pas partie de la classe des liens d'amitié.

$ 11.4 / 1- "... Le nom d'un ami n'est pas dans la portée de la classe, et l'ami n'est pas appelé avec les opérateurs d'accès aux membres (5.2.5) sauf s'il est membre d'une autre classe."

$ 11.4 - "De plus, parce que la clause de base de la classe friend ne fait pas partie de ses déclarations de membres, la clause de base de la classe friend ne peut pas accéder aux noms des membres privés et protégés de la classe accordant l'amitié."

et plus loin

$ 10.3 / 7- "[Remarque: le spécificateur virtuel implique l'appartenance, donc une fonction virtuelle ne peut pas être une fonction non membre (7.1.2). Une fonction virtuelle ne peut pas non plus être un membre statique, car un appel de fonction virtuelle repose sur un objet spécifique pour déterminer la fonction à appeler. Une fonction virtuelle déclarée dans une classe peut être déclarée amie dans une autre classe.] "

Puisque l '«ami» n'est pas membre de la classe de base en premier lieu, comment peut-il être hérité par la classe dérivée?

Chubsdad
la source
L'amitié, bien que donnée par des déclarations comme des membres, n'est pas vraiment des membres, mais des notifications dont les autres classes peuvent essentiellement ignorer les classifications de visibilité sur les membres «réels». Alors que les sections de spécifications que vous citez expliquent comment le langage fonctionne concernant ces nuances et encadre le comportement dans une terminologie auto-cohérente, les choses auraient pu être conçues différemment, et rien ci-dessus ne va malheureusement au cœur de la justification.
Jeff
0

La fonction Friend dans une classe affecte la propriété extern à la fonction. ie extern signifie que la fonction a été déclarée et définie quelque part hors de la classe.

Cela signifie donc que la fonction friend n'est pas membre d'une classe. L'héritage ne vous permet donc d'hériter que des propriétés d'une classe et non des choses externes. Et aussi si l'héritage est autorisé pour les fonctions d'ami, alors une classe tierce hérite.

Robert
la source
0

Friend est bon en héritage comme l'interface de style pour le conteneur Mais pour moi, comme le premier dit, C ++ manque d'héritage propageable

class Thing;

//an interface for Thing container's
struct IThing {
   friend Thing;
   protected:
       int IThing_getData() = 0;
};

//container for thing's
struct MyContainer : public IThing {
    protected: //here is reserved access to Thing
         int IThing_getData() override {...}
};

struct Thing {
    void setYourContainer(IThing* aContainerOfThings) {
        //access to unique function in protected area 
        aContainerOfThings->IThing_getData(); //authorized access
    }
};

struct ChildThing : public Thing {
    void doTest() {
        //here the lack of granularity, you cannot access to the container.
        //to use the container, you must implement all 
        //function in the Thing class
        aContainerOfThings->IThing_getData(); //forbidden access
    }
};

Pour moi, le problème du C ++ est le manque de très bonne granularité pour contrôler tous les accès de n'importe où pour n'importe quoi:

ami Thing peut devenir ami Thing. * pour accorder l'accès à tous les enfants de Thing

Et de plus, ami [zone nommée] Chose. * Pour accorder l'accès pour un précis sont dans la classe Conteneur via une zone nommée spéciale pour l'ami.

Ok arrête le rêve. Mais maintenant, vous connaissez une utilisation intéressante de l'ami.

Dans un autre ordre, vous pouvez également trouver intéressant de savoir que toutes les classes sont amicales avec soi-même. En d'autres termes, une instance de classe peut appeler tous les
membres d'une autre instance du même nom sans restriction:

class Object {
     private:
         void test() {}
     protected:
         void callAnotherTest(Object* anotherObject) {
             //private, but yes you can call test() from 
             //another object instance
             anotherObject)->test(); 
         }
};
user7268856
la source
0

Logique simple: «J'ai une amie Jane. Ce n'est pas parce que nous sommes devenus amis hier que tous ses amis sont miens.

Je dois encore approuver ces amitiés individuelles, et le niveau de confiance serait en conséquence.

Robert Hamm
la source