Perché il C ++ non consente l'amicizia ereditata?


93

Perché l'amicizia non è almeno facoltativamente ereditabile in C ++? Capisco che la transitività e la riflessività siano proibite per ovvie ragioni (lo dico solo per evitare semplici risposte alle domande frequenti), ma la mancanza di qualcosa sulla falsariga di virtual friend class Foo;me mi lascia perplesso. Qualcuno conosce il contesto storico dietro questa decisione? L'amicizia era davvero solo un trucco limitato che da allora ha trovato la sua strada in alcuni oscuri usi rispettabili?

Modifica per chiarimenti: sto parlando del seguente scenario, non in cui i figli di A sono esposti a B o sia a B che ai suoi figli. Posso anche immaginare di concedere facoltativamente l'accesso a sostituzioni di funzioni di amicizia, ecc.

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. */ };

Risposta accettata: come afferma Loki , l'effetto può essere simulato più o meno creando funzioni proxy protette in classi base amiche, quindi non è strettamente necessario garantire l'amicizia a una gerarchia di classe o metodo virtuale. Non mi piace la necessità di proxy boilerplate (che la base amica diventa effettivamente), ma suppongo che questo fosse ritenuto preferibile rispetto a un meccanismo linguistico che molto probabilmente sarebbe stato utilizzato in modo improprio la maggior parte del tempo. Penso che probabilmente sia giunto il momento di comprare e leggere The Design and Evolution of C ++ di Stroupstrup , che ho visto abbastanza persone qui consigliare, per ottenere una migliore comprensione di questo tipo di domande ...

Risposte:


93

Perché posso scrivere Fooe il suo amico Bar(quindi c'è un rapporto di fiducia).

Ma mi fido delle persone che scrivono lezioni da cui derivano Bar?
Non proprio. Quindi non dovrebbero ereditare l'amicizia.

Qualsiasi cambiamento nella rappresentazione interna di una classe richiederà una modifica a tutto ciò che dipende da quella rappresentazione. Quindi tutti i membri di una classe e anche tutti gli amici della classe richiederanno modifiche.

Quindi se la rappresentazione interna di Fooviene modificata allora Bardeve essere modificata anche (perché l'amicizia si lega strettamente Bara Foo). Se l'amicizia è stata ereditata, tutte le classi derivano daBar sarebbero strettamente legate Fooe quindi richiederebbero modifiche se Foola rappresentazione interna di è cambiata. Ma non ho alcuna conoscenza dei tipi derivati ​​(né dovrei I. Possono anche essere sviluppati da società diverse, ecc.). Pertanto non sarei in grado di modificare in Fooquanto ciò introdurrebbe modifiche di rottura nella base di codice (poiché non potrei modificare tutta la classe derivata daBar ).

Pertanto, se l'amicizia è stata ereditata, stai inavvertitamente introducendo una restrizione alla capacità di modificare una classe. Ciò è indesiderabile poiché sostanzialmente si rende inutile il concetto di API pubblica.

Nota: un figlio di Barpuò accedere Fooutilizzando Bar, basta inserire il metodoBar protetto. Quindi il bambino di Barpuò accedere a un fileFoo chiamando tramite la sua classe genitore.

È questo che vuoi?

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);
        }
};

4
Bingo. Si tratta di limitare il danno che puoi causare modificando gli interni di una classe.
j_random_hacker

Francamente, il caso a cui sto davvero pensando è il modello Attorney-Client, in cui un intermedio funge da interfaccia limitata per le classi esterne presentando metodi wrapper alla classe ad accesso limitato sottostante. Dire che un'interfaccia è disponibile per tutti i figli di altre classi piuttosto che una classe esatta sarebbe molto più utile del sistema attuale.
Jeff

@ Jeff: l'esposizione della rappresentazione interna a tutti i figli di una classe renderà il codice immutabile (in realtà interrompe anche l'incapsulamento poiché chiunque volesse accedere ai membri interni deve solo ereditare da Bar anche se non sono realmente un Bar ).
Martin York

@ Martin: Giusto, in questo schema la base di amici potrebbe essere utilizzata per accedere alla classe di amicizia, che potrebbe essere una semplice violazione dell'incapsulamento in molti (se non nella maggior parte) casi. Tuttavia, nelle situazioni in cui la base amica è una classe astratta, qualsiasi classe derivata sarebbe costretta a implementare una propria interfaccia reciproca. Non sono sicuro se una classe di "impostori" in questo scenario sarebbe considerata come una violazione dell'incapsulamento o una violazione di un contratto di interfaccia se non cercasse di agire fedelmente e in modo appropriato.
Jeff

@ Martin: Esatto, questo è l'effetto che desidero e talvolta uso già, dove A è in realtà un'interfaccia reciprocamente amica per una classe Z ad accesso limitato. La lamentela comune con il normale idioma avvocato-cliente sembra essere che la classe di interfaccia A deve boilerplate call wrapper a Z, e per estendere l'accesso alle sottoclassi, il boilerplate di A deve essere essenzialmente duplicato in ogni classe base come B. Un'interfaccia normalmente esprime quale funzionalità un modulo vuole offrire, non quale funzionalità in altri esso vuole usare se stesso.
Jeff

48

Perché l'amicizia non è almeno facoltativamente ereditabile in C ++?

Penso che la risposta alla tua prima domanda sia in questa domanda: "Gli amici di tuo padre hanno accesso ai tuoi privati?"


36
Ad essere onesti, questa domanda solleva questioni inquietanti su tuo padre. . .
iheanyi

3
Qual è il punto di questa risposta? nella migliore delle ipotesi un commento dubbio anche se forse leggero
DeveloperChris

11

Una classe amica può esporre il suo amico tramite funzioni accessorie e quindi concedere l'accesso tramite quelle.

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(); }
};

Ciò consente un controllo più preciso rispetto alla transitività opzionale. Ad esempio, get_cashpotrebbe essere protectedo potrebbe imporre un protocollo di accesso limitato al runtime.


@ Ettore: voto per il refactoring! ;)
Alexander Shukaev

7

Standard C ++, sezione 11.4 / 8

L'amicizia non è né ereditata né transitiva.

Se l'amicizia venisse ereditata, una classe che non era pensata per essere un amico avrebbe improvvisamente accesso alle parti interne della tua classe e ciò viola l'incapsulamento.


2
Dì Q "amici" A, e B deriva da A. Se B eredita l'amicizia da A, allora poiché B è un tipo di A, tecnicamente è un A che ha accesso ai privati ​​di Q. Quindi questo non risponde alla domanda con alcuna ragione pratica.
mdenton8

2

Perché è solo inutile.

L'uso della friendparola chiave è di per sé sospetto. In termini di accoppiamento è la relazione peggiore (molto più avanti dell'eredità e della composizione).

Qualsiasi modifica agli interni di una classe rischia di avere un impatto sugli amici di questa classe ... vuoi davvero un numero sconosciuto di amici? Non saresti nemmeno in grado di elencarli se coloro che ereditano da loro potessero essere anche amici, e correresti il ​​rischio di rompere il codice dei tuoi clienti ogni volta, sicuramente questo non è desiderabile.

Ammetto liberamente che per i compiti a casa / progetti di animali domestici la dipendenza è spesso una considerazione lontana. Su progetti di piccole dimensioni non importa. Ma non appena più persone lavorano allo stesso progetto e questo cresce in dozzine di migliaia di righe, è necessario limitare l'impatto dei cambiamenti.

Questo porta una regola molto semplice:

La modifica degli interni di una classe dovrebbe influire solo sulla classe stessa

Certo, probabilmente influenzerai i suoi amici, ma ci sono due casi qui:

  • funzione senza amici: probabilmente più di una funzione membro in ogni caso (penso std::ostream& operator<<(...) qui, che non è un membro puramente per caso delle regole della lingua
  • classe di amici? non hai bisogno di lezioni di amicizia su classi reali.

Suggerirei l'uso del metodo semplice:

class Example;

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

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

Questo semplice Keyschema ti consente di dichiarare un amico (in un certo modo) senza dargli effettivamente accesso ai tuoi interni, isolandolo così dai cambiamenti. Inoltre, consente a questo amico di prestare la sua chiave agli amministratori (come i bambini) se necessario.


0

Un'ipotesi: se una classe dichiara un'altra classe / funzione come amico, è perché quella seconda entità ha bisogno di un accesso privilegiato alla prima. A cosa serve concedere alla seconda entità l'accesso privilegiato a un numero arbitrario di classi derivate dalla prima?


2
Se una classe A volesse concedere amicizia a B e ai suoi discendenti, potrebbe evitare di aggiornare la sua interfaccia per ogni sottoclasse aggiunta o costringere B a scrivere boilerplate pass-through, che è la metà del punto di amicizia in primo luogo credo.
Jeff

@ Jeff: Ah, allora ho frainteso il tuo significato. Pensavo volessi dire che Bavrebbe accesso a tutte le classi ereditate da A...
Oliver Charlesworth

0

Una classe derivata può ereditare solo qualcosa, che è "membro" della base. Una dichiarazione di amicizia non è un membro della classe di amicizia.

$ 11.4 / 1- "... Il nome di un amico non è nell'ambito della classe e l'amico non viene chiamato con gli operatori di accesso ai membri (5.2.5) a meno che non sia un membro di un'altra classe."

$ 11.4 - "Inoltre, poiché la clausola base della classe friend non fa parte delle dichiarazioni dei suoi membri, la clausola base della classe friend non può accedere ai nomi dei membri privati ​​e protetti dalla classe che garantisce amicizia."

e inoltre

$ 10.3 / 7- "[Nota: lo specificatore virtuale implica l'appartenenza, quindi una funzione virtuale non può essere una funzione non membro (7.1.2). Né una funzione virtuale può essere un membro statico, poiché una chiamata di funzione virtuale si basa su un oggetto specifico per determinare quale funzione richiamare. Una funzione virtuale dichiarata in una classe può essere dichiarata amica in un'altra classe.] "

Dal momento che "l'amico" non è un membro della classe base in primo luogo, come può essere ereditato dalla classe derivata?


L'amicizia, sebbene data attraverso dichiarazioni come membri, non sono realmente membri tanto quanto notifiche di quali altre classi possono essenzialmente ignorare le classificazioni di visibilità sui membri "reali". Mentre le sezioni delle specifiche che citi spiegano come funziona il linguaggio riguardo a queste sfumature e al comportamento dei frame in una terminologia auto-coerente, le cose avrebbero potuto essere realizzate in modo diverso e, sfortunatamente, nulla di quanto sopra arriva al cuore della logica.
Jeff

0

La funzione Friend in una classe assegna la proprietà extern alla funzione. cioè extern significa che la funzione è stata dichiarata e definita da qualche parte fuori dalla classe.

Quindi significa che la funzione amico non è un membro di una classe. Quindi l'ereditarietà ti permette di ereditare solo le proprietà di una classe, non cose esterne. E anche se l'ereditarietà è consentita per le funzioni di amicizia, allora una classe di terze parti che eredita.


0

Friend è buono per l'ereditarietà come l'interfaccia di stile per il contenitore Ma per me, come primo dire, C ++ manca dell'ereditarietà propagabile

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
    }
};

Per me il problema di C ++ è la mancanza di un'ottima granularità per controllare tutti gli accessi da qualsiasi luogo per qualsiasi cosa:

Friend Thing può diventare Friend Thing. * per concedere l'accesso a tutti i figli di Thing

E ancora, amico [area denominata] Cosa. * Per concedere l'accesso per una posizione precisa nella classe Contenitore tramite un'area con nome speciale per l'amico.

Ok, ferma il sogno. Ma ora conosci un uso interessante dell'amico.

In un altro ordine, puoi anche trovare interessante sapere che tutte le classi sono amichevoli con se stesse. In altre parole, un'istanza di classe può chiamare tutti i
membri di un'altra istanza con lo stesso nome senza restrizioni:

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

0

Logica semplice: 'Ho un'amica Jane. Solo perché ieri siamo diventati amici non fa miei tutti i suoi amici ».

Devo ancora approvare quelle amicizie individuali e il livello di fiducia sarebbe di conseguenza.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.