Mengapa C ++ tidak mengizinkan persahabatan yang diwariskan?

95

Mengapa persahabatan tidak paling tidak diwariskan secara opsional dalam C ++? Saya memahami transitivitas dan refleksivitas dilarang karena alasan yang jelas (saya mengatakan ini hanya untuk menghindari jawaban kutipan FAQ sederhana), tetapi kurangnya sesuatu di sepanjang garis virtual friend class Foo;teka - teki saya. Adakah yang tahu latar belakang sejarah di balik keputusan ini? Apakah persahabatan benar-benar hanya peretasan terbatas yang sejak itu menemukan jalannya ke dalam beberapa kegunaan terhormat yang tidak jelas?

Edit untuk klarifikasi: Saya sedang berbicara tentang skenario berikut, bukan di mana anak-anak A terpapar pada B atau B dan anaknya. Saya juga dapat membayangkan secara opsional memberikan akses ke penggantian fungsi teman, dll.

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

Jawaban yang diterima: seperti yang dinyatakan Loki , efek dapat disimulasikan lebih atau kurang dengan membuat fungsi proxy yang dilindungi di kelas dasar yang bersahabat , jadi tidak ada kebutuhan yang ketat untuk memberikan pertemanan ke kelas atau metode virtual heirarki. Saya tidak menyukai kebutuhan akan proxy boilerplate (yang menjadi basis teman secara efektif), tetapi saya kira ini dianggap lebih disukai daripada mekanisme bahasa yang kemungkinan besar akan disalahgunakan sebagian besar waktu. Saya pikir mungkin sudah waktunya saya membeli dan membaca The Design and Evolution of C ++ dari Stroupstrup , yang saya rekomendasikan oleh cukup banyak orang di sini, untuk mendapatkan wawasan yang lebih baik tentang jenis pertanyaan ini ...

Jeff
sumber

Jawaban:

94

Karena saya boleh menulis Foodan temannya Bar(dengan demikian ada hubungan kepercayaan).

Tapi apakah saya mempercayai orang yang menulis kelas yang diturunkan darinya Bar?
Tidak juga. Jadi mereka seharusnya tidak mewarisi persahabatan.

Setiap perubahan dalam representasi internal kelas akan membutuhkan modifikasi apa pun yang bergantung pada representasi itu. Dengan demikian semua anggota kelas dan juga semua teman kelas akan memerlukan modifikasi.

Oleh karena itu jika representasi internal Fooyang diubah maka Barjuga harus diubah (karena persahabatan erat mengikat Barke Foo). Jika persahabatan diwariskan maka semua kelas yang diturunkan dari Barjuga akan terikat erat Foodan dengan demikian membutuhkan modifikasi jika Foorepresentasi internal diubah. Tetapi saya tidak memiliki pengetahuan tentang jenis turunan (saya juga tidak seharusnya. Mereka bahkan mungkin dikembangkan oleh perusahaan yang berbeda, dll). Jadi saya tidak akan dapat mengubah Fookarena melakukan hal itu akan memperkenalkan perubahan pemutusan ke dalam basis kode (karena saya tidak dapat memodifikasi semua kelas yang diturunkan dari Bar).

Jadi, jika persahabatan diwarisi, Anda secara tidak sengaja memasukkan batasan pada kemampuan untuk mengubah kelas. Ini tidak diinginkan karena pada dasarnya Anda menjadikan konsep API publik tidak berguna.

Catatan: Seorang anak dari Bardapat mengakses Foodengan menggunakan Bar, cukup buat metode ini Bardilindungi. Kemudian anak dari Bardapat mengakses a Foodengan memanggil melalui kelas induknya.

Apa ini yang kau inginkan?

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
sumber
4
Bingo. Ini semua tentang membatasi kerusakan yang dapat Anda sebabkan dengan mengubah internal kelas.
j_random_hacker
Sejujurnya, kasus yang benar-benar saya pikirkan adalah pola Pengacara-Klien, di mana perantara bertindak sebagai antarmuka terbatas ke kelas luar dengan menyajikan metode pembungkus ke kelas akses terbatas yang mendasarinya. Mengatakan bahwa sebuah antarmuka tersedia untuk semua turunan dari kelas lain daripada kelas yang tepat akan jauh lebih berguna daripada sistem saat ini.
Jeff
@Jeff: Mengekspos representasi internal ke semua turunan kelas akan menyebabkan kode menjadi tidak dapat diubah (itu juga benar-benar merusak enkapsulasi karena siapa pun yang ingin mengakses anggota internal hanya harus mewarisi dari Bar meskipun mereka sebenarnya bukan Bar ).
Martin York
@Martin: Benar, dalam skema ini basis pertemanan dapat digunakan untuk masuk ke kelas pertemanan, yang bisa menjadi pelanggaran enkapsulasi sederhana dalam banyak (jika tidak sebagian besar) kasus. Namun, dalam situasi di mana basis teman adalah kelas abstrak, kelas turunan apa pun akan dipaksa untuk mengimplementasikan antarmuka timbal baliknya sendiri. Saya tidak yakin apakah kelas 'penipu' dalam skenario ini akan dianggap melanggar enkapsulasi atau melanggar kontrak antarmuka jika tidak mencoba untuk dengan setia menjalankan peran yang diklaimnya dengan benar.
Jeff
@Martin: Benar, itu adalah efek yang saya inginkan dan kadang-kadang sudah benar-benar digunakan, di mana A sebenarnya adalah antarmuka yang berteman secara timbal balik ke beberapa kelas akses terbatas Z. Keluhan umum dengan Idiom Pengacara-Klien yang normal tampaknya adalah bahwa kelas antarmuka A harus melakukan boilerplate call wrappers ke Z, dan untuk memperluas akses ke subclass, boilerplate A pada dasarnya harus diduplikasi di setiap kelas dasar seperti B. Antarmuka biasanya mengungkapkan fungsionalitas apa yang ingin ditawarkan modul, bukan fungsionalitas apa di modul lain. ingin menggunakan dirinya sendiri.
Jeff
48

Mengapa persahabatan tidak paling tidak diwariskan secara opsional dalam C ++?

Saya pikir jawaban untuk pertanyaan pertama Anda ada di pertanyaan ini: "Apakah teman ayahmu memiliki akses ke kemaluanmu?"

wilx
sumber
36
Agar adil, pertanyaan itu menimbulkan pertanyaan yang mengganggu tentang ayahmu. . .
iheanyi
3
Apa gunanya jawaban ini? komentar meragukan meskipun mungkin ringan hati
DeveloperChris
11

Kelas teman dapat mengekspos temannya melalui fungsi pengakses, dan kemudian memberikan akses melalui fungsi tersebut.

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

Ini memungkinkan kontrol yang lebih baik daripada transitivitas opsional. Misalnya, get_cashmungkin protectedatau mungkin menegakkan protokol akses terbatas waktu proses.

Potatoswatter
sumber
@Hector: Voting untuk refactoring! ;)
Alexander Shukaev
8

C ++ Standard, bagian 11.4 / 8

Persahabatan tidak diwariskan atau transitif.

Jika persahabatan akan diwarisi, maka kelas yang tidak dimaksudkan sebagai teman akan tiba-tiba memiliki akses ke internal kelas Anda dan melanggar enkapsulasi.

David
sumber
2
Katakanlah Q "teman" A, dan B diturunkan dari A. Jika B mewarisi pertemanan dari A, maka karena B adalah tipe A, secara teknis IS an A yang memiliki akses ke pribadi Q. Jadi ini tidak menjawab pertanyaan dengan alasan praktis apa pun.
mdenton8
2

Karena itu tidak perlu.

Penggunaan file friend kata kunci itu sendiri mencurigakan. Dalam hal penggandengan, itu adalah hubungan terburuk (jauh di depan warisan dan komposisi).

Setiap perubahan pada internal kelas berisiko berdampak pada teman di kelas ini ... apakah Anda benar-benar menginginkan jumlah teman yang tidak diketahui? Anda bahkan tidak akan dapat mencantumkannya jika mereka yang mewarisi dari mereka bisa menjadi teman juga, dan Anda berisiko memecahkan kode klien Anda setiap saat, tentunya ini tidak diinginkan.

Saya dengan bebas mengakui bahwa untuk pekerjaan rumah / proyek hewan peliharaan ketergantungan seringkali merupakan pertimbangan yang jauh. Pada proyek ukuran kecil tidak masalah. Tetapi segera setelah beberapa orang mengerjakan proyek yang sama dan ini berkembang menjadi puluhan ribu baris, Anda perlu membatasi dampak perubahan.

Ini membawa aturan yang sangat sederhana:

Mengubah internal kelas seharusnya hanya memengaruhi kelas itu sendiri

Tentu saja, Anda mungkin akan memengaruhi teman-temannya, tetapi ada dua kasus di sini:

  • fungsi bebas teman: mungkin lebih dari fungsi anggota pula (saya pikir di std::ostream& operator<<(...)sini, yang bukan anggota murni karena kebetulan aturan bahasa
  • kelas teman? Anda tidak perlu kelas teman di kelas nyata.

Saya akan merekomendasikan penggunaan metode sederhana:

class Example;

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

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

KeyPola sederhana ini memungkinkan Anda untuk menyatakan seorang teman (dengan cara tertentu) tanpa benar-benar memberinya akses ke internal Anda, sehingga mengisolasinya dari perubahan. Selanjutnya memungkinkan teman ini untuk meminjamkan kuncinya kepada wali (seperti anak-anak) jika diperlukan.

Matthieu M.
sumber
0

Tebakan: Jika sebuah kelas mendeklarasikan kelas / fungsi lain sebagai teman, itu karena entitas kedua membutuhkan akses istimewa ke yang pertama. Apa gunanya memberikan entitas kedua akses istimewa ke sejumlah kelas yang diturunkan dari yang pertama?

Oliver Charlesworth
sumber
2
Jika kelas A ingin memberikan pertemanan kepada B dan keturunannya, ia dapat menghindari pembaruan antarmuka untuk setiap subkelas yang ditambahkan atau memaksa B untuk menulis boilerplate pass-through, yang menurut saya setengah dari poin persahabatan.
Jeff
@Jeff: Ah, kalau begitu aku salah paham maksudmu. Saya berasumsi maksud Anda yang Bakan memiliki akses ke semua kelas yang diwarisi dari A...
Oliver Charlesworth
0

Kelas turunan hanya dapat mewarisi sesuatu, yaitu 'anggota' basis. Pernyataan teman bukanlah anggota kelas pertemanan.

$ 11.4 / 1- "... Nama teman tidak termasuk dalam lingkup kelas, dan teman tersebut tidak dipanggil dengan operator akses anggota (5.2.5) kecuali jika itu adalah anggota kelas lain."

$ 11.4 - "Juga, karena klausa dasar dari kelas teman bukan bagian dari deklarasi anggotanya, klausa dasar dari kelas teman tidak dapat mengakses nama anggota privat dan dilindungi dari kelas yang memberikan pertemanan."

dan selanjutnya

$ 10.3 / 7- "[Catatan: penentu virtual menyiratkan keanggotaan, jadi fungsi virtual tidak dapat menjadi fungsi nonanggota (7.1.2). Fungsi virtual juga tidak dapat menjadi anggota statis, karena panggilan fungsi virtual bergantung pada objek tertentu untuk menentukan fungsi mana yang akan dipanggil. Fungsi virtual yang dideklarasikan di satu kelas dapat dinyatakan sebagai teman di kelas lain.] "

Karena 'teman' bukanlah anggota kelas dasar, bagaimana bisa diturunkan oleh kelas turunan?

Chubsdad
sumber
Persahabatan, meskipun diberikan melalui deklarasi seperti anggota, sebenarnya bukanlah anggota seperti halnya pemberitahuan di mana kelas lain pada dasarnya dapat mengabaikan klasifikasi visibilitas pada anggota 'nyata'. Meskipun bagian spesifikasi yang Anda kutip menjelaskan cara kerja bahasa terkait nuansa dan perilaku bingkai ini dalam terminologi yang konsisten dengan dirinya sendiri, hal-hal tersebut dapat dibuat secara berbeda, dan sayangnya, tidak ada hal di atas yang masuk ke inti pemikirannya.
Jeff
0

Fungsi teman di kelas menetapkan properti eksternal ke fungsi tersebut. yaitu extern berarti bahwa fungsi tersebut telah dideklarasikan dan didefinisikan di suatu tempat di luar kelas.

Oleh karena itu, artinya fungsi teman bukan anggota kelas. Jadi, warisan hanya memungkinkan Anda untuk mewarisi properti kelas, bukan hal eksternal. Dan juga jika warisan diperbolehkan untuk fungsi teman, maka kelas pihak ketiga mewarisi.

Robert
sumber
0

Teman baik dalam hal warisan seperti antarmuka gaya untuk wadah Tapi bagi saya, sebagai kata pertama, C ++ tidak memiliki warisan yang dapat disebarkan

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

Bagi saya masalah C ++ adalah kurangnya perincian yang sangat baik untuk mengontrol semua akses dari mana saja untuk apa pun:

teman Hal dapat menjadi Hal teman. * untuk memberikan akses ke semua anak Benda

Dan lebih banyak lagi, teman [area bernama] Hal. * Untuk memberikan akses yang tepat berada di kelas Kontainer melalui area bernama khusus untuk teman.

Ok hentikan mimpinya. Tapi sekarang, Anda tahu kegunaan teman yang menarik.

Di urutan lain, Anda juga bisa menemukan hal menarik untuk diketahui semua kelas ramah terhadap diri sendiri. Dengan kata lain, instance kelas dapat memanggil semua
anggota instance lain dengan nama yang sama tanpa batasan:

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

Logika sederhana: 'Saya punya teman Jane. Hanya karena kita menjadi teman kemarin tidak membuat semua temannya menjadi milikku. '

Saya masih perlu menyetujui persahabatan individu itu, dan tingkat kepercayaannya akan sesuai.

Robert Hamm
sumber