Menggunakan kelas teman untuk merangkum fungsi anggota pribadi di C ++ - praktik atau penyalahgunaan yang baik?

12

Jadi saya perhatikan mungkin untuk menghindari menempatkan fungsi pribadi di header dengan melakukan sesuatu seperti ini:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        friend class PredicateList_HelperFunctions;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList_HelperFunctions
    {
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList_HelperFunctions::fullMatch(*this);
    }

Fungsi pribadi tidak pernah dideklarasikan di header, dan konsumen kelas yang mengimpor header tidak perlu tahu itu ada. Ini diperlukan jika fungsi helper adalah templat (alternatifnya adalah meletakkan kode lengkap di header), itulah cara saya "menemukan" ini. Kelebihan bagus lainnya dari tidak perlu mengkompilasi ulang setiap file yang menyertakan header jika Anda menambah / menghapus / memodifikasi fungsi anggota pribadi. Semua fungsi pribadi ada dalam file .cpp.

Begitu...

  1. Apakah ini pola desain terkenal yang ada namanya?
  2. Bagi saya (berasal dari latar belakang Java / C # dan belajar C ++ pada waktu saya sendiri), ini sepertinya hal yang sangat baik, karena header mendefinisikan antarmuka, sedangkan .cpp mendefinisikan implementasi (dan waktu kompilasi yang ditingkatkan adalah bonus yang bagus). Namun, baunya seperti menyalahgunakan fitur bahasa yang tidak dimaksudkan untuk digunakan seperti itu. Jadi, mana yang benar? Apakah ini sesuatu yang Anda tidak suka melihat dalam proyek C ++ profesional?
  3. Adakah jebakan yang tidak saya pikirkan?

Saya menyadari Pimpl, yang merupakan cara yang jauh lebih kuat untuk menyembunyikan implementasi di tepi perpustakaan. Ini lebih untuk digunakan dengan kelas internal, di mana Pimpl akan menyebabkan masalah kinerja, atau tidak berfungsi karena kelas perlu diperlakukan sebagai nilai.


EDIT 2: Jawaban sempurna Dragon Energy di bawah ini menyarankan solusi berikut, yang sama sekali tidak menggunakan friendkata kunci:

// In file pred_list.h:
    class PredicateList
    {
        int somePrivateField;
        class Private;
    public:
        bool match();
    } 

// In file pred_list.cpp:
    class PredicateList::Private
    {
    public:
        static bool fullMatch(PredicateList& p)
        {
            return p.somePrivateField == 5; // or whatever
        }
    }

    bool PredicateList::match()
    {
        return PredicateList::Private::fullMatch(*this);
    }

Ini menghindari faktor kejut dari friend(yang tampaknya telah di-iblis seperti goto) sambil tetap mempertahankan prinsip pemisahan yang sama.

Robert Fraser
sumber
2
" Seorang konsumen dapat menentukan kelas mereka sendiri PredicateList_HelperFunctions, dan biarkan mereka mengakses bidang pribadi. " Bukankah itu merupakan pelanggaran ODR ? Baik Anda dan konsumen harus mendefinisikan kelas yang sama. Jika definisi-definisi itu tidak sama, maka kodenya salah bentuk.
Nicol Bolas

Jawaban:

13

Agak terlalu esoteris untuk mengatakan setidaknya ketika Anda sudah mengenali yang mungkin membuat saya menggaruk-garuk kepala sejenak ketika saya pertama kali mulai menemukan kode Anda bertanya-tanya apa yang Anda lakukan dan di mana kelas-kelas pembantu ini diterapkan sampai saya mulai mengambil gaya Anda / Kebiasaan (pada titik mana saya mungkin akan benar-benar terbiasa).

Saya suka Anda mengurangi jumlah informasi di header. Terutama dalam basis kode yang sangat besar, yang dapat memiliki efek praktis untuk mengurangi dependensi waktu kompilasi dan akhirnya membangun waktu.

Reaksi usus saya adalah bahwa jika Anda merasa perlu untuk menyembunyikan detail implementasi dengan cara ini, untuk mendukung parameter lewat ke fungsi yang berdiri sendiri dengan tautan internal dalam file sumber. Biasanya Anda dapat mengimplementasikan fungsi utilitas (atau seluruh kelas) yang berguna untuk mengimplementasikan kelas tertentu tanpa memiliki akses ke semua internal kelas dan alih-alih hanya meneruskan yang relevan dari penerapan metode ke fungsi (atau konstruktor). Dan tentu saja itu memiliki bonus mengurangi kopling antara kelas Anda dan "pembantu". Ini juga memiliki kecenderungan untuk menggeneralisasi apa yang seharusnya menjadi "pembantu" lebih lanjut jika Anda menemukan bahwa mereka mulai melayani tujuan yang lebih umum yang berlaku untuk implementasi lebih dari satu kelas.

Saya juga terkadang merasa ngeri sedikit ketika saya melihat banyak "pembantu" dalam kode. Itu tidak selalu benar tetapi kadang-kadang mereka dapat merupakan gejala dari pengembang yang hanya membusuk fungsi mau tak mau untuk menghilangkan duplikasi kode dengan gumpalan besar data yang diteruskan ke fungsi dengan nama / tujuan yang hampir tidak dapat dipahami di luar fakta bahwa mereka mengurangi jumlah diperlukan kode untuk mengimplementasikan beberapa fungsi lainnya. Hanya sedikit lebih banyak berpikir di muka kadang-kadang dapat menyebabkan kejelasan yang lebih besar dalam hal bagaimana implementasi kelas didekomposisi menjadi fungsi lebih lanjut, dan lebih suka melewati parameter tertentu untuk melewati seluruh contoh objek Anda dengan akses penuh ke internal dapat membantu mempromosikan gaya pemikiran desain itu. Saya tidak menyarankan Anda melakukan itu, tentu saja (saya tidak tahu),

Jika itu menjadi sulit, saya akan mempertimbangkan solusi kedua, lebih idiomatis yang merupakan jerawat (saya menyadari Anda menyebutkan masalah dengan itu tetapi saya pikir Anda dapat menggeneralisasi solusi untuk menghindari mereka dengan usaha minimal). Itu bisa memindahkan seluruh banyak informasi yang perlu diimplementasikan oleh kelas Anda termasuk data privatnya dari grosir header. Masalah kinerja pimpl sebagian besar dapat dikurangi dengan pengalokasi waktu-murah konstan * seperti daftar gratis sambil menjaga semantik nilai tanpa harus menerapkan ctor copy yang ditentukan pengguna full-blown.

  • Untuk aspek kinerja, mucikari memperkenalkan overhead pointer setidaknya, tapi saya pikir kasus harus cukup serius di mana pose menjadi perhatian praktis. Jika lokalitas spasial tidak terdegradasi secara signifikan melalui pengalokasi, maka loop ketat Anda berulang di atas objek (yang umumnya harus homogen jika kinerjanya sangat memprihatinkan) masih akan cenderung meminimalkan cache yang hilang dalam praktik asalkan Anda menggunakan sesuatu seperti daftar gratis untuk mengalokasikan jerawat, menempatkan bidang kelas ke dalam blok memori yang berdekatan.

Secara pribadi hanya setelah menguras kemungkinan itu saya akan mempertimbangkan sesuatu seperti ini. Saya pikir itu ide yang baik jika alternatifnya seperti metode yang lebih pribadi yang diekspos ke header dengan mungkin hanya sifat esoterik yang menjadi perhatian praktis.

Sebuah alternatif

Salah satu alternatif yang muncul di kepala saya sekarang yang sebagian besar mencapai tujuan yang sama Anda tidak ada teman adalah seperti ini:

struct PredicateListData
{
     int somePrivateField;
};

class PredicateList
{
    PredicateListData data;
public:
    bool match() const;
};

// In source file:
static bool fullMatch(const PredicateListData& p)
{
     // Can access p.somePrivateField here.
}

bool PredicateList::match() const
{
     return fullMatch(data);
}

Nah, itu mungkin tampak seperti perbedaan yang sangat bisa diperdebatkan dan saya masih menyebutnya "penolong" (dalam arti yang mungkin merendahkan karena kita masih menyerahkan seluruh keadaan internal kelas ke fungsi apakah perlu semuanya atau tidak) kecuali itu menghindari faktor "kejutan" dari pertemuan friend. Secara umum friendterlihat agak menakutkan untuk melihat sering tidak ada inspeksi lebih lanjut, karena ia mengatakan bahwa internal kelas Anda dapat diakses di tempat lain (yang membawa implikasi bahwa ia mungkin tidak mampu mempertahankan invariannya sendiri). Dengan cara Anda menggunakannya friendmenjadi agak diperdebatkan jika orang menyadari praktik sejak saat itufriendhanya berada di file sumber yang sama yang membantu mengimplementasikan fungsionalitas pribadi kelas, tetapi hal di atas menghasilkan banyak efek yang sama setidaknya dengan manfaat yang mungkin diperdebatkan bahwa itu tidak melibatkan teman yang menghindari semua jenis itu ("Oh tembak, kelas ini punya teman. Di mana lagi privatnya bisa diakses / dimutasi? "). Sedangkan versi di atas segera mengkomunikasikan bahwa tidak ada cara bagi privat untuk diakses / dimutasi di luar apa pun yang dilakukan dalam implementasi PredicateList.

Itu mungkin bergerak menuju wilayah yang agak dogmatis dengan tingkat nuansa ini karena siapa pun dapat dengan cepat mengetahui apakah Anda secara seragam menyebutkan berbagai hal *Helper*dan menempatkan semuanya dalam file sumber yang sama yang semuanya dibundel bersama sebagai bagian dari implementasi privat sebuah kelas. Tetapi jika kita menjadi rewel, mungkin gaya yang tepat di atas tidak akan menyebabkan reaksi spontan sekilas tidak ada friendkata kunci yang cenderung terlihat sedikit menakutkan.

Untuk pertanyaan lain:

Seorang konsumen dapat mendefinisikan PredicateList_HelperFunctions kelas mereka sendiri, dan membiarkan mereka mengakses bidang pribadi. Meskipun saya tidak melihat ini sebagai masalah besar (jika Anda benar-benar ingin di bidang-bidang pribadi itu Anda bisa melakukan casting), mungkin itu akan mendorong konsumen untuk menggunakannya seperti itu?

Itu mungkin merupakan kemungkinan lintas batas API di mana klien dapat menentukan kelas kedua dengan nama yang sama dan mendapatkan akses ke internal seperti itu tanpa kesalahan tautan. Kemudian lagi saya sebagian besar C coder bekerja di grafik di mana masalah keamanan pada tingkat "bagaimana jika" ini sangat rendah pada daftar prioritas, jadi kekhawatiran seperti ini hanya yang saya cenderung melambaikan tangan dan menari dan cobalah untuk berpura-pura seolah mereka tidak ada. :-D Jika Anda bekerja di domain di mana masalah seperti ini agak serius, saya pikir itu pertimbangan yang layak untuk dibuat.

Usulan alternatif di atas juga menghindari masalah ini. Jika Anda masih ingin tetap menggunakan friend, Anda juga dapat menghindari masalah itu dengan menjadikan helper sebagai kelas bersarang pribadi.

class PredicateList
{
    ...

    // Declare nested class.
    class Helper;

    // Make it a friend.
    friend class Helper;

public:
    ...
};

// In source file:
class PredicateList::Helper
{
    ...
};

Apakah ini pola desain terkenal yang ada namanya?

Tidak ada yang sepengetahuan saya. Saya agak ragu akan ada satu karena itu benar-benar masuk ke detail detail dan gaya implementasi.

"Helper Hell"

Saya mendapat permintaan klarifikasi lebih lanjut tentang bagaimana saya terkadang merasa ngeri ketika saya melihat implementasi dengan banyak kode "pembantu", dan itu mungkin sedikit kontroversial dengan beberapa tetapi sebenarnya faktual karena saya benar-benar merasa ngeri ketika saya debug beberapa implementasi kolega saya di kelas hanya untuk menemukan banyak "pembantu". :-D Dan aku bukan satu-satunya di tim yang menggaruk-garuk kepalaku mencoba untuk mencari tahu apa yang seharusnya dilakukan semua penolong ini. Saya juga tidak ingin lepas dari dogmatis seperti "Jangan menggunakan pembantu," tetapi saya akan membuat saran kecil bahwa mungkin membantu untuk memikirkan bagaimana menerapkan hal-hal yang tidak ada pada mereka ketika praktis.

Tidak semua fungsi anggota pribadi membantu fungsi dengan definisi?

Dan ya, saya termasuk metode pribadi. Jika saya melihat kelas dengan antarmuka publik yang langsung tetapi seperti serangkaian metode pribadi yang tak terbatas yang agak tidak jelas tujuannya seperti find_implatau find_detailatau find_helper, maka saya juga merasa ngeri dengan cara yang sama.

Apa yang saya sarankan sebagai alternatif adalah fungsi non-teman nonanggota dengan tautan internal (dideklarasikan staticatau di dalam namespace anonim) untuk membantu mengimplementasikan kelas Anda dengan setidaknya tujuan yang lebih umum daripada "fungsi yang membantu mengimplementasikan orang lain". Dan saya dapat mengutip Herb Sutter dari C ++ 'Coding Standards' di sini untuk alasan mengapa itu lebih disukai dari sudut pandang SE umum:

Hindari biaya keanggotaan: Jika memungkinkan, lebih suka membuat fungsi yang bukan anggota bukan teman. [...] Fungsi non-teman non-anggota meningkatkan enkapsulasi dengan meminimalkan dependensi: Tubuh fungsi tidak dapat bergantung pada anggota kelas yang bukan publik dari kelas (lihat Item 11). Mereka juga memecah kelas monolitik untuk membebaskan fungsi yang dapat dipisahkan, lebih lanjut mengurangi kopling (lihat Item 33).

Anda juga dapat memahami "biaya keanggotaan" yang dibicarakannya sampai batas tertentu dalam hal prinsip dasar penyempitan ruang lingkup variabel. Jika Anda membayangkan, sebagai contoh paling ekstrem, objek Dewa yang memiliki semua kode yang diperlukan untuk menjalankan seluruh program Anda, maka pilih "pembantu" jenis ini (fungsi, apakah fungsi anggota atau teman) yang dapat mengakses semua internal ( privat) dari suatu kelas pada dasarnya membuat variabel-variabel itu tidak kurang bermasalah daripada variabel global. Anda memiliki semua kesulitan mengelola keadaan dengan benar dan menjaga keamanan serta mempertahankan invarian yang akan Anda dapatkan dengan variabel global dalam contoh paling ekstrem ini. Dan tentu saja sebagian besar contoh nyata mudah-mudahan tidak mendekati ekstrem ini, tetapi menyembunyikan informasi hanya berguna karena membatasi ruang lingkup informasi yang diakses.

Sekarang Sutter sudah memberikan penjelasan yang bagus di sini, tetapi saya juga menambahkan bahwa decoupling cenderung mempromosikan seperti peningkatan psikologis (setidaknya jika otak Anda bekerja seperti milik saya) dalam hal bagaimana Anda merancang fungsi. Ketika Anda mulai mendesain fungsi yang tidak dapat mengakses semua yang ada di kelas kecuali hanya parameter yang relevan yang Anda lewati atau, jika Anda lulus instance dari kelas sebagai parameter, hanya anggota publiknya, ia cenderung mempromosikan pola pikir desain yang mendukung fungsi yang memiliki tujuan yang lebih jelas, di atas decoupling dan mempromosikan enkapsulasi yang lebih baik, daripada apa yang Anda mungkin tergoda untuk merancang jika Anda bisa mengakses semuanya.

Jika kita kembali ke ekstremitas maka basis kode yang penuh dengan variabel global tidak benar-benar menggoda pengembang untuk merancang fungsi dengan cara yang jelas dan digeneralisasi untuk tujuan tertentu. Sangat cepat, semakin banyak informasi yang dapat Anda akses dalam suatu fungsi, semakin banyak dari kita manusia menghadapi godaan untuk merosotkannya dan mengurangi kejernihannya dalam hal mengakses semua informasi tambahan yang kita miliki daripada menerima parameter yang lebih spesifik dan relevan dengan fungsi itu. untuk mempersempit aksesnya ke negara dan memperluas penerapannya dan meningkatkan kejelasan niatnya. Itu berlaku (meskipun umumnya pada tingkat yang lebih rendah) dengan fungsi anggota atau teman.

Energi Naga
sumber
1
Terima kasih atas masukannya! Saya tidak sepenuhnya mengerti dari mana Anda berasal dengan bagian ini, meskipun: "Saya juga terkadang merasa ngeri ketika melihat banyak" pembantu "dalam kode." - Bukankah semua fungsi anggota pribadi membantu fungsi menurut definisi? Ini tampaknya membawa masalah dengan fungsi anggota pribadi pada umumnya.
Robert Fraser
1
Ah, kelas dalam sama sekali tidak membutuhkan "teman", jadi melakukannya dengan cara yang benar-benar menghindari kata kunci "teman"
Robert Fraser
"Bukankah semua fungsi anggota privat membantu fungsi berdasarkan definisi? Ini sepertinya mempermasalahkan fungsi anggota pribadi secara umum." Itu bukan yang terbesar. Dulu saya berpikir itu adalah kebutuhan praktis bahwa untuk implementasi kelas non-sepele, Anda juga memiliki sejumlah fungsi pribadi atau pembantu dengan akses ke semua anggota kelas sekaligus. Tapi saya melihat gaya beberapa hebat seperti Linus Torvalds, John Carmack, dan meskipun kode sebelumnya dalam C, ketika dia mengkodekan persamaan analog dari suatu objek, dia berhasil umumnya mengkodekannya dengan tidak bersama dengan Carmack.
Dragon Energy
Dan tentu saja saya pikir pembantu dalam file sumber lebih disukai daripada beberapa header besar yang mencakup header lebih banyak daripada yang diperlukan karena menggunakan banyak fungsi pribadi untuk membantu mengimplementasikan kelas. Tetapi setelah mempelajari gaya dari yang di atas dan yang lain, saya menyadari bahwa seringkali mungkin untuk menulis fungsi yang sedikit lebih digeneralisasi daripada tipe yang membutuhkan akses ke semua anggota internal suatu kelas bahkan untuk hanya mengimplementasikan satu kelas, dan pemikiran di muka untuk menyebutkan fungsi dengan baik dan memberikannya anggota spesifik yang dibutuhkan untuk bekerja sering akhirnya menghemat lebih banyak waktu [...]
Dragon Energy
[...] daripada yang dibutuhkan, menghasilkan implementasi yang lebih jelas secara keseluruhan yang lebih mudah untuk dimanipulasi nantinya. Ini seperti alih-alih menulis "predikat pembantu" untuk "pertandingan penuh" yang mengakses semua yang ada di Anda PredicateList, sering kali mungkin layak untuk hanya meneruskan satu atau dua anggota dari daftar predikat ke fungsi yang sedikit lebih umum yang tidak memerlukan akses ke setiap anggota pribadi PredicateList, dan seringkali yang cenderung cenderung juga menghasilkan nama dan tujuan yang lebih jelas dan lebih umum untuk fungsi internal itu serta lebih banyak peluang untuk "penggunaan kembali kode belakang".
Dragon Energy