Panduan Gaya C ++ Google. Bagian 4

Bagian 1. Pendahuluan

...

Bagian 4. Kelas

...





Artikel ini adalah terjemahan dari bagian panduan gaya C ++ Google ke dalam bahasa Rusia.

Artikel asli (bercabang di github), terjemahan diperbarui .





Kelas





Kelas adalah blok bangunan utama dalam C ++. Dan, tentu saja, sering digunakan. Bagian ini menjelaskan aturan dasar dan larangan yang harus diikuti saat menggunakan kelas.



Kode di konstruktor





Jangan panggil metode virtual dalam konstruktor. Hindari inisialisasi yang dapat gagal (dan tidak ada cara untuk menandakan kesalahan. Catatan: Harap diperhatikan bahwa Google tidak menyukai pengecualian).



Definisi



Secara umum, setiap inisialisasi dapat dilakukan dalam konstruktor (yaitu semua inisialisasi dapat dilakukan dalam konstruktor).



Per



  • Tidak perlu khawatir tentang kelas yang tidak diinisialisasi.
  • Objek yang sepenuhnya diinisialisasi dalam konstruktor dapat berupa konst dan juga lebih mudah digunakan dalam wadah standar dan algoritme.




Vs



  • Jika fungsi virtual dipanggil dalam konstruktor, maka implementasi dari kelas turunan tidak dipanggil. Bahkan jika kelas sekarang tidak memiliki keturunan, ini mungkin menjadi masalah di masa depan.
  • ( ) ( ).
  • , ( — ) . : bool IsValid(). .
  • . , , .








Konstruktor Putusan tidak boleh memanggil fungsi virtual. Dalam beberapa kasus (jika diperbolehkan), kesalahan desain dapat ditangani melalui penghentian program. Jika tidak, pertimbangkan pola Metode Pabrik atau gunakan Init () (detail selengkapnya di sini: TotW # 42 ). Gunakan Init () hanya jika objek tersebut memiliki bendera negara yang memungkinkan pemanggilan fungsi publik tertentu (karena sulit untuk sepenuhnya bekerja dengan objek yang dibangun sebagian).



Konversi implisit





Jangan mendeklarasikan konversi implisit. Gunakan kata kunci eksplisit untuk operator konversi tipe dan konstruktor argumen tunggal.



Definisi



Konversi implisit memungkinkan objek dari satu tipe sumber untuk digunakan di mana tipe lain (tipe tujuan) diharapkan, seperti meneruskan argumen tipe int ke fungsi yang mengharapkan ganda .



Selain konversi implisit yang ditentukan oleh bahasa pemrograman, Anda juga dapat menentukan konversi kustom Anda sendiri dengan menambahkan anggota yang sesuai ke deklarasi kelas (sumber dan tujuan). Konversi implisit sisi sumber dideklarasikan sebagai jenis operator + penerima (misalnya, operator bool () ). Konversi implisit di sisi penerima diimplementasikan oleh konstruktor yang menggunakan tipe sumber sebagai satu-satunya argumen (selain argumen default).



Kata kunci eksplisit dapat diterapkan ke konstruktor atau operator konversi untuk secara eksplisit menunjukkan bahwa fungsi hanya dapat digunakan jika ada kecocokan tipe eksplisit (misalnya, setelah operasi cast). Ini berlaku tidak hanya untuk konversi implisit, tetapi juga untuk daftar inisialisasi di C ++ 11:



class Foo {
  explicit Foo(int x, double y);
  ...
};
void Func(Foo f);

      
      







Func({42, 3.14});  // 

      
      







Contoh kode ini secara teknis bukanlah konversi implisit, tetapi bahasa memperlakukannya seolah-olah itu dimaksudkan untuk menjadi eksplisit .



Per



  • , .
  • , string_view std::string const char*.
  • .








  • , ( ).
  • , : , .
  • , .
  • explicit : , .
  • , . — , .
  • , , , .




Operator Verdict



Konversi dan konstruktor argumen tunggal harus dideklarasikan dengan kata kunci eksplisit . Ada juga pengecualian: konstruktor salin dan pindahkan dapat dideklarasikan tanpa eksplisit , karena mereka tidak melakukan konversi tipe. Selain itu, konversi implisit mungkin diperlukan dalam kasus kelas pembungkus untuk jenis lain (dalam kasus ini, pastikan untuk meminta izin manajemen upstream Anda untuk mengabaikan aturan penting ini).



Konstruktor yang tidak dapat dipanggil dengan satu argumen dapat dideklarasikan tanpa eksplisit . Konstruktor menerima satu std :: initializer_listjuga harus dideklarasikan tanpa eksplisit untuk mendukung copy-inisialisasi (misalnya, MyType m = {1, 2}; ).



Jenis yang dapat disalin dan direlokasi





Antarmuka publik kelas harus secara eksplisit menunjukkan kemampuan untuk menyalin dan / atau memindahkan, atau sebaliknya, melarang semuanya. Mendukung penyalinan dan / atau pemindahan hanya jika operasi ini masuk akal untuk jenis Anda.



Definisi



Jenis yang dapat direlokasi adalah jenis yang dapat diinisialisasi atau ditetapkan dari nilai sementara.



Jenis yang dapat disalin - dapat diinisialisasi atau ditetapkan dari objek lain dengan jenis yang sama (yaitu, sama dengan yang dapat direlokasi), asalkan objek asli tetap tidak berubah. Misalnya , std :: unique_ptr <int> dapat direlokasi, tetapi tidak jenis yang akan disalin (karena nilai objek std :: unique_ptr <int> asli harus berubah ketika ditetapkan ke target). intdan std :: string adalah contoh tipe yang dapat direlokasi yang juga dapat disalin: untuk int operasi pemindahan dan penyalinan adalah sama, untuk std :: string operasi pemindahan memerlukan lebih sedikit sumber daya daripada salinan.



Untuk tipe yang ditentukan pengguna, penyalinan ditentukan oleh pembuat salinan dan operator penyalinan. Gerakan ditentukan baik oleh konstruktor pemindahan dengan operator pemindahan, atau (jika tidak ada) oleh fungsi penyalinan yang sesuai.



Menyalin dan memindahkan konstruktor dapat secara implisit dipanggil oleh kompilator, misalnya, saat meneruskan objek berdasarkan nilai.



Per



Objek dengan tipe yang dapat disalin dan direlokasi dapat diteruskan dan diterima oleh nilai, yang membuat API lebih sederhana, lebih aman, lebih fleksibel. Dalam hal ini, tidak ada masalah dengan kepemilikan objek, siklus hidupnya, perubahan nilai, dll., Dan juga tidak diharuskan untuk menentukannya dalam "kontrak" (semua ini tidak seperti melewatkan objek dengan pointer atau referensi). Komunikasi yang malas antara klien dan implementasi juga dicegah, membuat kode lebih mudah dipahami, dipelihara, dan dioptimalkan oleh compiler. Objek semacam itu dapat digunakan sebagai argumen ke kelas lain yang memerlukan penerusan nilai (misalnya, sebagian besar wadah), dan secara umum lebih fleksibel (misalnya, saat digunakan dalam pola desain).



Salin / pindahkan konstruktor dan operator tugas terkait biasanya lebih mudah ditentukan daripada alternatif seperti Clone () , CopyFrom (), atau Swap () karena kompilator dapat menghasilkan fungsi yang diperlukan (secara implisit atau dengan = default ). Mereka (fungsi) mudah dideklarasikan dan Anda dapat yakin bahwa semua anggota kelas akan disalin. Konstruktor (menyalin dan memindahkan) umumnya lebih efisien karena tidak memerlukan alokasi memori, inisialisasi terpisah, tugas tambahan, dioptimalkan dengan baik (lihat penghapusan salinan ).



Operator bergerak memungkinkan Anda untuk secara efisien (dan secara implisit) memanipulasi nilai r sumber daya objek. Ini terkadang membuat pengkodean lebih mudah.



Terhadap



Beberapa jenis tidak diperlukan untuk dapat disalin, dan dukungan untuk operasi penyalinan mungkin kontra-intuitif atau menyebabkan operasi yang salah. Jenis untuk singletones ( Registerer ), objek untuk dibersihkan (misalnya, saat keluar dari ruang lingkup) ( Cleanup ) atau berisi data unik ( Mutex ), dalam artinya, tidak dapat disalin. Selain itu, operasi penyalinan untuk kelas dasar yang memiliki turunan dapat menyebabkan pemotongan objek... Operasi penyalinan default (atau ditulis dengan buruk) dapat menyebabkan kesalahan yang sulit dideteksi.



Konstruktor salinan dipanggil secara implisit dan ini mudah untuk dilupakan (terutama untuk programmer yang sebelumnya menulis dalam bahasa di mana objek diteruskan dengan referensi). Anda juga dapat mengurangi kinerja dengan membuat salinan yang tidak perlu.



Putusan



Antarmuka publik setiap kelas harus secara eksplisit menunjukkan operasi salin dan / atau pemindahan yang didukungnya. Ini biasanya dilakukan di bagian publik dalam bentuk deklarasi eksplisit dari fungsi yang diperlukan atau dengan mendeklarasikannya sebagai hapus.



Secara khusus, kelas yang disalin harus secara eksplisit menyatakan operasi penyalinan; hanya kelas yang dapat direlokasi yang harus secara eksplisit mendeklarasikan operasi pemindahan; kelas yang tidak dapat disalin / tidak dapat dipindahkan harus secara eksplisit menolak ( = menghapus ) operasi penyalinan. Mendeklarasikan atau menghapus secara eksplisit keempat fungsi salin dan pindahkan juga diperbolehkan, meskipun tidak diperlukan. Jika Anda mengimplementasikan operator copy dan / atau move, maka Anda juga harus membuat konstruktor yang sesuai.



class Copyable {
 public:
  Copyable(const Copyable& other) = default;
  Copyable& operator=(const Copyable& other) = default;
  //       (..  )
};
class MoveOnly {
 public:
  MoveOnly(MoveOnly&& other);
  MoveOnly& operator=(MoveOnly&& other);
  //     .  ( )    :
  MoveOnly(const MoveOnly&) = delete;
  MoveOnly& operator=(const MoveOnly&) = delete;
};
class NotCopyableOrMovable {
 public:
  //       
  NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
  NotCopyableOrMovable& operator=(const NotCopyableOrMovable&)
      = delete;
  //     (),    :
  NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
  NotCopyableOrMovable& operator=(NotCopyableOrMovable&&)
      = delete;
};

      
      







Deklarasi atau penghapusan fungsi yang dijelaskan dapat dihilangkan dalam kasus yang jelas:



  • Jika kelas tidak berisi bagian pribadi (misalnya, struct atau kelas antarmuka), maka copyability dan relokasi dapat dideklarasikan melalui properti serupa dari setiap anggota publik.
  • , . , , .
  • , () /, / (.. ). / . .




Suatu tipe tidak boleh dinyatakan dapat disalin / direlokasi kecuali jika programmer biasa memahami kebutuhan untuk operasi ini, atau jika operasi sangat intensif sumber daya dan kinerja. Operasi pemindahan untuk jenis yang disalin selalu merupakan pengoptimalan kinerja, tetapi di sisi lain, operasi ini berpotensi menjadi sumber bug dan komplikasi. Oleh karena itu, jangan nyatakan operasi pemindahan kecuali operasi tersebut memberikan peningkatan kinerja yang signifikan atas penyalinan. Secara umum, diinginkan (jika operasi penyalinan dideklarasikan untuk suatu kelas) untuk mendesain semuanya sehingga fungsi penyalinan default digunakan. Dan pastikan untuk memeriksa kebenaran operasi apa pun secara default.



Karena risiko "pemotongan", sebaiknya hindari operator penyalinan dan pemindahan publik untuk kelas yang Anda rencanakan untuk digunakan sebagai kelas dasar (dan sebaiknya tidak mewarisi dari kelas dengan fungsi seperti itu). Jika Anda perlu membuat kelas dasar dapat disalin, buat fungsi virtual publik Clone () dan konstruktor salinan yang dilindungi sehingga kelas turunan dapat menggunakannya untuk mengimplementasikan operasi penyalinan.



Struktur vs Kelas





Gunakan struct hanya untuk objek pasif yang menyimpan data. Dalam kasus lain, gunakan kelas ( kelas ).



Kata kunci struct dan class hampir identik di C ++. Namun, kami memiliki pemahaman sendiri untuk setiap kata kunci, jadi gunakan kata kunci yang sesuai dengan tujuan dan artinya.



Struktur harus digunakan untuk objek pasif, hanya untuk transfer data. Mereka dapat memiliki konstanta sendiri, tetapi tidak boleh ada fungsionalitas apa pun (dengan kemungkinan pengecualian fungsi get / set). Semua bidang harus publik, tersedia untuk akses langsung, dan ini lebih disukai daripada menggunakan fungsi get / set. Struktur tidak boleh berisi invarian (misalnya, nilai yang dihitung) yang didasarkan pada ketergantungan antara berbagai bidang struktur: kemampuan untuk langsung mengubah bidang dapat membatalkan invarian. Metode tidak boleh membatasi penggunaan struktur, tetapi dapat menetapkan nilai ke bidang: mis. sebagai konstruktor, destruktor atau fungsi Initialize () , Reset () .



Jika fungsionalitas tambahan diperlukan dalam pemrosesan data atau invarian, lebih disukai menggunakan kelas ( kelas ). Juga, jika ragu mana yang harus dipilih - gunakan kelas.



Dalam beberapa kasus ( template meta-function , traits, beberapa functors) untuk konsistensi dengan STL, diperbolehkan menggunakan struktur sebagai pengganti kelas.



Ingatlah bahwa variabel dalam struktur dan kelas diberi nama dengan gaya yang berbeda.



Struktur vs pasangan dan tupel





Jika elemen individu dalam blok data dapat diberi nama yang bermakna, maka diinginkan untuk menggunakan struktur daripada pasangan atau tupel.



Saat menggunakan pasangan dan tupel menghindari menciptakan kembali roda dengan tipe Anda sendiri dan akan menghemat banyak waktu untuk menulis kode, bidang dengan nama yang bermakna (bukan .first , .second, atau std :: get <X> ) akan lebih mudah dibaca saat membaca kode. Dan meskipun C ++ 14 menambahkan akses tipe ( std :: get <Type> , dan tipenya harus unik) selain akses indeks untuk tupel , nama field jauh lebih informatif daripada tipenya.



Pair dan tuple bisa digunakan dalam kode di mana tidak ada perbedaan khusus antara elemen pair atau tuple. Mereka juga diharuskan untuk bekerja dengan kode atau API yang ada.



Warisan





Komposisi kelas seringkali lebih tepat daripada warisan. Saat menggunakan warisan, publikasikan .



Definisi



Ketika kelas anak mewarisi dari kelas dasar, itu termasuk definisi dari semua data dan operasi dari basis. Pewarisan antarmuka adalah pewarisan dari kelas dasar abstrak murni (tidak ada status atau metode yang ditentukan di dalamnya). Yang lainnya adalah "warisan implementasi".



Per



Pewarisan implementasi mengurangi ukuran kode dengan menggunakan kembali bagian dari kelas dasar (yang menjadi bagian dari kelas baru). Karena inheritance adalah deklarasi waktu kompilasi, yang memungkinkan kompilator untuk memahami struktur dan menemukan kesalahan. Pewarisan antarmuka dapat digunakan untuk membuat kelas mendukung API yang diperlukan. Dan juga, kompilator dapat menemukan kesalahan jika kelas tidak menentukan metode yang diperlukan dari API yang diwariskan.



Kontra



Dalam kasus pewarisan implementasi, kode mulai kabur antara kelas dasar dan anak dan ini dapat mempersulit pemahaman kode. Selain itu, kelas anak tidak dapat mengganti kode fungsi non-virtual (tidak dapat mengubah implementasinya).



Pewarisan ganda bahkan lebih bermasalah dan terkadang menyebabkan penurunan kinerja. Seringkali, penalti performa saat berpindah dari single inheritance ke multiple inheritance bisa lebih besar daripada transisi dari fungsi biasa ke fungsi virtual. Ini juga merupakan satu langkah dari multiple inheritance ke rhombic inheritance, dan ini sudah mengarah pada ambiguitas, kebingungan dan, tentu saja, bug.



Putusan



Setiap warisan harus publik . Jika Anda ingin membuatnya menjadi pribadi , lebih baik menambahkan anggota baru dengan instance kelas dasar.



Jangan terlalu sering menggunakan warisan implementasi. Komposisi kelas sering lebih disukai. Cobalah untuk membatasi penggunaan semantik pewarisan "adalah»: Bar , Anda dapat mewarisi dari Foo , jika saya boleh mengatakan bahwa Bar "adalah» Foo (yaitu, jika menggunakan Foo , Anda juga dapat menggunakan Bar ).



Dilindungi ( dilindungi, ) hanya melakukan fungsi-fungsi yang seharusnya tersedia untuk kelas anak. Perhatikan bahwa data harus bersifat pribadi.



Secara eksplisit mendeklarasikan penggantian fungsi / penghancur virtual menggunakan penentu: baik mengganti atau (jika diperlukan) final . Jangan gunakan penentu virtual saat mengganti fungsi. Penjelasan: Sebuah fungsi atau destruktor yang ditandai override atau final tetapi bukan virtual tidak akan bisa dikompilasi (yang membantu menangkap kesalahan umum). Penentu juga bekerja seperti dokumentasi; dan jika tidak ada penentu, pemrogram akan dipaksa untuk memeriksa seluruh hierarki untuk mengklarifikasi fungsi virtual.



Beberapa warisan diperbolehkan, namun banyak warisan implementasi tidak disarankan dari kata sama sekali.



Operator kelebihan beban





Membebani operator dengan alasan yang wajar. Jangan gunakan literal khusus.



Penentuan



kode C ++ memungkinkan pengguna untuk mengganti operator built-in menggunakan operator kata kunci dan tipe pengguna sebagai salah satu parameter; juga operator memungkinkan Anda untuk mendefinisikan literal baru menggunakan operator "" ; Anda juga dapat membuat fungsi casting seperti operator bool () .



Per



Menggunakan operator overloading untuk tipe yang ditentukan pengguna (mirip dengan tipe built-in) dapat membuat kode Anda lebih ringkas dan intuitif. Operator yang kelebihan beban berhubungan dengan operasi tertentu (misalnya, == , < , = dan << ) dan jika kode mengikuti logika penerapan operasi ini, maka tipe yang ditentukan pengguna dapat dibuat lebih jelas dan digunakan saat bekerja dengan pustaka eksternal yang mengandalkan operasi ini.



Literal kustom adalah cara yang sangat efisien untuk membuat objek kustom.



Vs



  • (, ) — , , .
  • , .
  • , , .
  • , , .
  • , .
  • / ( ), «» . , foo < bar &foo < &bar; .
  • . & , . &&, || , () ( ) .
  • , . , .
  • (UDL) , C++ . : «Hello World»sv std::string_view(«Hello World»). , .
  • Karena tidak ada namespace yang ditentukan untuk UDL, Anda harus menggunakan direktif using (yang dilarang ) atau deklarasi using (yang juga dilarang (di file header) , kecuali nama yang diimpor adalah bagian dari antarmuka yang ditampilkan di file header). Untuk file header seperti itu, yang terbaik adalah menghindari sufiks UDL, dan sebaiknya hindari ketergantungan antara literal yang berbeda dalam file header dan sumber.




Putusan



Definisikan operator yang kelebihan beban hanya jika artinya jelas, dapat dimengerti, dan konsisten dengan logika umum. Misalnya, gunakan | dalam arti operasi OR; mengimplementasikan logika pipa bukan ide yang bagus.



Tentukan operator hanya untuk jenis Anda sendiri, lakukan di header dan file sumber yang sama, dan di namespace yang sama. Akibatnya, operator akan tersedia di tempat yang sama dengan tipe itu sendiri, dan risiko definisi ganda minimal. Jika memungkinkan, hindari menetapkan operator sebagai templat. Anda harus mencocokkan semua argumen template. Jika Anda mendefinisikan operator, tentukan juga "saudara" untuknya. Dan jaga konsistensi hasil yang mereka kembalikan. Misalnya, jika Anda mendefinisikan operator < , maka tentukan semua operator perbandingan dan pastikan bahwa operator < dan > tidak pernah mengembalikan true untuk argumen yang sama.



Sebaiknya mendefinisikan operator biner yang tidak dapat diubah sebagai fungsi eksternal (non-anggota). Jika operator biner dideklarasikan sebagai anggota kelas, konversi implisit dapat diterapkan ke argumen kanan, tetapi tidak ke argumen kiri. Ini bisa sedikit membuat frustasi bagi programmer jika (misalnya) kode a <b akan dikompilasi, tetapi b <a tidak akan .



Tidak perlu mencoba melewati penggantian operator. Jika perbandingan (atau penugasan dan fungsi keluaran) diperlukan, lebih baik untuk mendefinisikan == (atau = dan << ) daripada Equals () , CopyFrom () dan PrintTo () . Sebaliknya, Anda tidak perlu mendefinisikan ulang operator hanya karena perpustakaan eksternal mengharapkannya. Misalnya, jika tipe data tidak bisa diurutkan dan Anda ingin menyimpannya di std :: set , maka lebih baik membuat fungsi perbandingan kustom dan tidak menggunakan operator < .



Jangan timpa && , || , , (Comma) atau unary & . Jangan timpa operator "" , yaitu jangan tambahkan literal Anda sendiri. Jangan gunakan literal yang ditentukan sebelumnya (termasuk pustaka standar dan seterusnya).



Informasi tambahan:

Jenis konversi dijelaskan di bagian tentang konversi implisit . Operator = ditulis dalam konstruktor salinan . Topik overloading << untuk bekerja dengan aliran tercakup dalam aliran . Anda juga dapat membiasakan diri dengan aturan dari bagian tentang kelebihan beban fungsi , yang juga cocok untuk operator.



Mengakses anggota kelas





Selalu buat data kelas menjadi pribadi , kecuali konstanta . Ini menyederhanakan penggunaan invarian dengan menambahkan fungsi akses yang paling sederhana (seringkali konstan).



Ini diizinkan untuk mendeklarasikan data kelas sebagai dilindungi untuk digunakan dalam kelas pengujian (misalnya, saat menggunakan Google Test ) atau kasus serupa lainnya.



Prosedur pengumuman





Tempatkan iklan serupa di satu tempat, tampilkan bagian yang umum.



Definisi kelas biasanya dimulai dengan bagian dari masyarakat: , berjalan lebih lanjut dilindungi: dan kemudian pribadi: . Jangan tentukan bagian kosong.



Dalam setiap bagian, kelompokkan pernyataan serupa. Urutan yang disukai adalah tipe (termasuk typedef , penggunaan , kelas dan struktur bersarang), konstanta, metode pabrik, konstruktor, operator penugasan, destruktor, metode lain, anggota data.



Jangan tempatkan definisi metode besar dalam definisi kelas. Biasanya hanya metode yang sepele, sangat pendek, atau kinerja-kritis yang "dimasukkan" ke dalam definisi kelas. Lihat juga Fungsi Inline .



All Articles