Dlaczego C ++ nie pozwala na odziedziczoną przyjaźń?

95

Dlaczego przyjaźń nie jest przynajmniej opcjonalnie dziedziczona w C ++? Rozumiem, że przechodniość i refleksyjność są zabronione z oczywistych powodów (mówię to tylko po to, by odrzucić proste odpowiedzi na FAQ), ale brak czegoś w rodzaju virtual friend class Foo;zagadek mnie zaskakuje. Czy ktoś zna historyczne tło tej decyzji? Czy przyjaźń naprawdę była tylko ograniczonym hackiem, który od tamtej pory znalazł zastosowanie w kilku niejasnych, szanowanych zastosowaniach?

Edytuj dla wyjaśnienia: mówię o następującym scenariuszu, a nie o sytuacji, w której dzieci A są narażone na B lub zarówno B, jak i jego dzieci. Mogę sobie również wyobrazić opcjonalne przyznanie dostępu do nadpisań funkcji znajomych itp.

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

Zaakceptowana odpowiedź: jak stwierdza Loki , efekt można symulować mniej więcej przez tworzenie chronionych funkcji proxy w zaprzyjaźnionych klasach bazowych, więc nie ma ścisłej potrzeby przyznawania przyjaźni z dziedziczeniem klas lub metod wirtualnych. Nie podoba mi się potrzeba używania standardowych serwerów proxy (którymi faktycznie staje się zaprzyjaźniona baza), ale przypuszczam, że uznano to za lepsze od mechanizmu językowego, który byłby częściej nadużywany przez większość czasu. Myślę, że nadszedł czas, abym kupił i przeczytał książkę Stroupstrup The Design and Evolution of C ++ , którą poleca wystarczająco dużo osób, aby uzyskać lepszy wgląd w tego typu pytania ...

Jeff
źródło

Odpowiedzi:

94

Ponieważ mogę pisać Fooi jego przyjaciel Bar(stąd jest relacja zaufania).

Ale czy ufam ludziom, którzy piszą klasy, z których pochodzą Bar?
Nie całkiem. Dlatego nie powinni dziedziczyć przyjaźni.

Każda zmiana w wewnętrznej reprezentacji klasy będzie wymagała modyfikacji wszystkiego, co jest zależne od tej reprezentacji. Zatem wszyscy członkowie klasy, a także wszyscy przyjaciele klasy będą wymagali modyfikacji.

Dlatego też, jeśli wewnętrzne reprezentacja Foojest modyfikowany a następnie Barmusi być zmodyfikowany (ponieważ przyjaźń ściśle wiąże Barsię Foo). Gdyby przyjaźń była dziedziczona, wówczas wszystkie klasy wywodzące się z niej Barbyłyby również ściśle powiązane, Fooa zatem wymagałyby modyfikacji, gdyby Foozmieniła się wewnętrzna reprezentacja. Ale nie mam wiedzy na temat typów pochodnych (ani nie powinienem.) Mogą być nawet opracowane przez różne firmy itp.). W związku z tym nie byłbym w stanie tego zmienić, Fooponieważ spowodowałoby to wprowadzenie istotnych zmian w bazie kodu (ponieważ nie mógłbym zmodyfikować wszystkich klas pochodnych Bar).

Tak więc, jeśli przyjaźń została odziedziczona, nieumyślnie wprowadzasz ograniczenie możliwości modyfikowania klasy. Jest to niepożądane, ponieważ zasadniczo unieważniasz koncepcję publicznego interfejsu API.

Uwaga: dziecko Barmoże uzyskać dostęp Fooza pomocą Bar, po prostu ustaw metodę jako Barchronioną. Następnie dziecko Barmoże uzyskać dostęp do a Foo, wywołując przez swoją klasę nadrzędną.

Czy to jest to, czego chcesz?

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
źródło
4
Bingo. Chodzi o ograniczenie szkód, które możesz spowodować, zmieniając wewnętrzne elementy klasy.
j_random_hacker,
Szczerze mówiąc, przypadkiem, o którym naprawdę myślę, jest wzorzec Attorney-Client, w którym pośrednik działa jako ograniczony interfejs do klas zewnętrznych, prezentując metody opakowujące do podstawowej klasy z ograniczonym dostępem. Powiedzenie, że interfejs jest dostępny dla wszystkich elementów potomnych innych klas, a nie dokładnej klasy, byłoby znacznie bardziej przydatne niż obecny system.
Jeff,
@Jeff: Ujawnienie wewnętrznej reprezentacji wszystkim elementom podrzędnym klasy spowoduje, że kod stanie się niezmienny (w rzeczywistości psuje też hermetyzację, ponieważ każdy, kto chciał uzyskać dostęp do wewnętrznych elementów członkowskich, musi tylko dziedziczyć po Bar, nawet jeśli tak naprawdę nie jest to Bar ).
Martin York,
@Martin: Tak, w tym schemacie zaprzyjaźniona baza może zostać użyta do uzyskania dostępu do klasy zaprzyjaźnionej, co może być prostym naruszeniem hermetyzacji w wielu (jeśli nie w większości) przypadków. Jednak w sytuacjach, w których zaprzyjaźniona baza jest klasą abstrakcyjną, każda klasa pochodna zostanie zmuszona do implementacji własnego wzajemnego interfejsu. Nie jestem pewien, czy klasa „oszusta” w tym scenariuszu byłaby uznawana za naruszającą hermetyzację, czy też naruszającą kontrakt interfejsu, gdyby nie próbowała wiernie odgrywać deklarowanej roli.
Jeff,
@Martin: Racja, to jest efekt, którego chcę i czasami już używam, gdzie A jest w rzeczywistości wzajemnie zaprzyjaźnionym interfejsem do jakiejś ograniczonej klasy dostępu Z. Typowa skarga w przypadku normalnego idiomu prawnika-klienta wydaje się być taka, że ​​klasa interfejsu A musi tworzyć standardowe opakowania wywołań do Z, a aby rozszerzyć dostęp do podklas, schemat A musi być zasadniczo zduplikowany w każdej klasie bazowej, takiej jak B. Interfejs zwykle wyraża, jaką funkcjonalność moduł chce oferować, a nie jaką funkcjonalność w innych. chce się wykorzystać.
Jeff,
48

Dlaczego przyjaźń nie jest przynajmniej opcjonalnie dziedziczona w C ++?

Myślę, że odpowiedź na twoje pierwsze pytanie brzmi: "Czy przyjaciele twojego ojca mają dostęp do twoich szeregowych?"

wilx
źródło
36
Szczerze mówiąc, to pytanie rodzi niepokojące pytania dotyczące twojego ojca. . .
iheanyi
3
Jaki jest sens tej odpowiedzi? w najlepszym razie wątpliwy, choć prawdopodobnie lekki komentarz
DeveloperChris
11

Zaprzyjaźniona klasa może ujawnić swojego znajomego za pomocą funkcji akcesorów, a następnie udzielić dostępu za ich pośrednictwem.

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

Pozwala to na lepszą kontrolę niż opcjonalna przechodniość. Na przykład, get_cashmoże być protectedlub może dochodzić do protokołu dostępu wykonawczego ograniczony.

Potatoswatter
źródło
@Hector: Głosowanie za refaktoryzacją! ;)
Alexander Shukaev,
8

C ++ Standard, sekcja 11.4 / 8

Przyjaźń nie jest dziedziczna ani przechodnia.

Gdyby przyjaźń została odziedziczona, klasa, która nie miała być przyjacielem, nagle uzyskałaby dostęp do wewnętrznych elementów Twojej klasy, co narusza hermetyzację.

David
źródło
2
Powiedzmy, że Q „przyjaciele” A, a B pochodzi od A. Jeśli B dziedziczy przyjaźń po A, to ponieważ B jest typem A, technicznie rzecz biorąc, jest to A, który ma dostęp do szeregowych Q. Więc to nie odpowiada na pytanie z żadnego praktycznego powodu.
mdenton8
2

Ponieważ to jest po prostu niepotrzebne.

Użycie friendsłowa kluczowego samo w sobie jest podejrzane. Pod względem sprzężenia to najgorszy związek (znacznie wyprzedzający dziedziczenie i skład).

Każda zmiana w wnętrzu klasy może mieć wpływ na przyjaciół z tej klasy… czy naprawdę chcesz mieć nieznaną liczbę przyjaciół? Nie byłbyś nawet w stanie ich wymienić, gdyby ci, którzy po nich dziedziczą, mogliby być również przyjaciółmi, a za każdym razem ryzykowałbyś złamanie kodu swoich klientów, z pewnością nie jest to pożądane.

Przyznaję, że w przypadku prac domowych / projektów domowych zależność jest często odległa. W przypadku małych projektów nie ma to znaczenia. Ale gdy tylko kilka osób pracuje nad tym samym projektem, a ten rozrósł się do dziesiątek tysięcy linii, musisz ograniczyć wpływ zmian.

Prowadzi to do bardzo prostej zasady:

Zmiana elementów wewnętrznych klasy powinna wpływać tylko na samą klasę

Oczywiście prawdopodobnie wpłyniesz na jego znajomych, ale są tu dwa przypadki:

  • funkcja wolna od przyjaciela: prawdopodobnie i tak więcej funkcji składowej (myślę, że std::ostream& operator<<(...)tutaj, która nie jest członkiem wyłącznie przez przypadek reguł języka
  • klasa przyjaciela? nie potrzebujesz zajęć z przyjaciółmi na prawdziwych zajęciach.

Poleciłbym użycie prostej metody:

class Example;

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

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

Ten prosty Keywzorzec pozwala na zadeklarowanie przyjaciela (w pewnym sensie) bez faktycznego przyznawania mu dostępu do twoich wewnętrznych, tym samym izolując go od zmian. Ponadto pozwala temu przyjacielowi pożyczyć swój klucz powiernikom (takim jak dzieci), jeśli jest to wymagane.

Matthieu M.
źródło
0

Przypuszczenie: jeśli klasa deklaruje inną klasę / funkcję jako znajomego, to dlatego, że ta druga jednostka potrzebuje uprzywilejowanego dostępu do pierwszej. Jaki jest pożytek z przyznania drugiej jednostce uprzywilejowanego dostępu do dowolnej liczby klas wywodzących się z pierwszej?

Oliver Charlesworth
źródło
2
Gdyby klasa A chciała zapewnić przyjaźń B i jego potomkom, mogłaby uniknąć aktualizacji swojego interfejsu dla każdej dodanej podklasy lub zmuszania B do pisania schematu tranzytowego, co wydaje mi się, że jest to połowa punktu przyjaźni.
Jeff
@Jeff: Ach, więc źle zrozumiałem twoje zamierzone znaczenie. Zakładałem, że miałeś na myśli, że Bbędzie miał dostęp do wszystkich klas odziedziczonych po A...
Oliver Charlesworth
0

Klasa pochodna może dziedziczyć tylko coś, co jest „członkiem” bazy. Deklaracja znajomego nie jest członkiem klasy zaprzyjaźniającej się.

$ 11.4 / 1- "... Imię znajomego nie znajduje się w zakresie klasy, a znajomy nie jest wywoływany z operatorami dostępu do elementów członkowskich (5.2.5), chyba że jest członkiem innej klasy."

11,4 $ - "Ponadto, ponieważ klauzula podstawowa klasy zaprzyjaźnionej nie jest częścią jej deklaracji składowych, klauzula podstawowa klasy zaprzyjaźnionej nie może uzyskać dostępu do nazw członków prywatnych i chronionych z klasy zapewniającej przyjaźń."

i dalej

10,3 $ / 7- "[Uwaga: specyfikator wirtualny implikuje członkostwo, więc funkcja wirtualna nie może być funkcją niebędącą składową (7.1.2). Funkcja wirtualna nie może być również statycznym składnikiem, ponieważ wywołanie funkcji wirtualnej zależy od określonego obiektu dla określanie funkcji do wywołania. Funkcja wirtualna zadeklarowana w jednej klasie może zostać uznana za przyjaciela w innej klasie.] "

Skoro „przyjaciel” nie jest po pierwsze członkiem klasy bazowej, w jaki sposób może być dziedziczony przez klasę pochodną?

Chubsdad
źródło
Przyjaźń, chociaż jest udzielana poprzez deklaracje, takie jak członkowie, tak naprawdę nie są członkami, a raczej powiadomieniami o tym, które inne klasy mogą zasadniczo zignorować klasyfikację widoczności „prawdziwych” członków. Podczas gdy cytowane przez ciebie sekcje specyfikacji wyjaśniają, w jaki sposób język działa w odniesieniu do tych niuansów i zachowań ramek w spójnej terminologii, rzeczy mogły zostać opracowane inaczej i niestety nic powyżej nie dociera do sedna uzasadnienia.
Jeff,
0

Funkcja znajomego w klasie przypisuje do funkcji właściwość extern. tzn. extern oznacza, że ​​funkcja została zadeklarowana i zdefiniowana gdzieś poza klasą.

Stąd oznacza to, że funkcja zaprzyjaźniona nie jest członkiem klasy. Dziedziczenie pozwala więc dziedziczyć tylko właściwości klasy, a nie rzeczy zewnętrzne. A także jeśli dziedziczenie jest dozwolone dla funkcji zaprzyjaźnionych, to dziedziczenie klasy innej firmy.

Robert
źródło
0

Przyjaciel jest dobry w dziedziczeniu jak interfejs w stylu dla kontenera Ale dla mnie, jak pierwszy powiedział, C ++ nie ma dziedziczenia propagowalnego

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

Dla mnie problemem C ++ jest brak bardzo dobrej szczegółowości, aby kontrolować dostęp z dowolnego miejsca do czegokolwiek:

przyjaciel Rzecz może stać się przyjacielem Rzecz. *, aby zapewnić dostęp wszystkim dzieciom Rzeczy

Co więcej, przyjaciel [nazwany obszar] Rzecz. *, Aby przyznać dostęp do precyzyjnego, znajdują się w klasie kontenera za pośrednictwem specjalnego obszaru nazwanego dla znajomego.

Ok, zatrzymaj sen. Ale teraz znasz ciekawe zastosowanie znajomego.

W innej kolejności można również uznać za interesujące, że wszystkie klasy są przyjazne dla siebie. Innymi słowy, instancja klasy może
bez ograniczeń wywołać wszystkich członków innej instancji o tej samej nazwie:

class Object {
     private:
         void test() {}
     protected:
         void callAnotherTest(Object* anotherObject) {
             //private, but yes you can call test() from 
             //another object instance
             anotherObject)->test(); 
         }
};
user7268856
źródło
0

Prosta logika: „Mam przyjaciółkę Jane. To, że wczoraj zostaliśmy przyjaciółmi, nie sprawia, że ​​wszyscy jej przyjaciele są moimi.

Nadal muszę zaakceptować te indywidualne przyjaźnie, a poziom zaufania byłby odpowiedni.

Robert Hamm
źródło