Mengapa orang menggunakan kelas bersarang di C ++?

188

Dapatkah seseorang tolong tunjukkan saya pada beberapa sumber yang bagus untuk memahami dan menggunakan kelas bersarang? Saya memiliki beberapa materi seperti Prinsip-prinsip Pemrograman dan hal-hal seperti Pusat Pengetahuan IBM ini - Kelas Bertingkat

Tapi saya masih kesulitan memahami tujuan mereka. Bisakah seseorang tolong saya?

berkaca mata
sumber
15
Saran saya untuk kelas bersarang di C ++ hanyalah untuk tidak menggunakan kelas bersarang.
Billy ONeal
7
Mereka persis seperti kelas reguler ... kecuali bersarang. Gunakan ketika implementasi internal suatu kelas sangat kompleks sehingga paling mudah dimodelkan oleh beberapa kelas yang lebih kecil.
meagar
12
@Illy: Kenapa? Tampaknya terlalu luas bagi saya.
John Dibling
30
Saya masih belum melihat argumen mengapa kelas bersarang buruk menurut sifatnya.
John Dibling
7
@ 7vies: 1. karena tidak diperlukan - Anda dapat melakukan hal yang sama dengan kelas yang ditentukan secara eksternal, yang mengurangi cakupan variabel apa pun yang diberikan, yang merupakan hal yang baik. 2. karena Anda dapat melakukan semua yang bisa dilakukan oleh kelas bersarang typedef. 3. karena mereka menambahkan tingkat tambahan lekukan dalam lingkungan di mana menghindari antrean panjang sudah sulit 4. karena Anda menyatakan dua benda konseptual terpisah dalam satu classdeklarasi, dll
Billy Oneal

Jawaban:

229

Kelas bersarang itu keren untuk menyembunyikan detail implementasi.

Daftar:

class List
{
    public:
        List(): head(nullptr), tail(nullptr) {}
    private:
        class Node
        {
              public:
                  int   data;
                  Node* next;
                  Node* prev;
        };
    private:
        Node*     head;
        Node*     tail;
};

Di sini saya tidak ingin mengekspos Node karena orang lain mungkin memutuskan untuk menggunakan kelas dan itu akan menghalangi saya untuk memperbarui kelas saya karena apa pun yang terpapar adalah bagian dari API publik dan harus dipertahankan selamanya . Dengan menjadikan kelas privat, saya tidak hanya menyembunyikan implementasinya, saya juga mengatakan ini milik saya dan saya dapat mengubahnya kapan saja sehingga Anda tidak dapat menggunakannya.

Lihatlah std::listatau std::mapmereka semua mengandung kelas tersembunyi (atau apakah mereka?). Intinya adalah mereka mungkin atau mungkin tidak, tetapi karena implementasinya bersifat pribadi dan tersembunyi, pembangun STL dapat memperbarui kode tanpa mempengaruhi bagaimana Anda menggunakan kode, atau meninggalkan banyak bagasi lama tergeletak di sekitar STL karena mereka membutuhkan untuk mempertahankan kompatibilitas dengan beberapa orang bodoh yang memutuskan mereka ingin menggunakan kelas Node yang tersembunyi di dalamnya list.

Martin York
sumber
9
Jika Anda melakukan ini maka Nodeseharusnya tidak diekspos di file header sama sekali.
Billy ONeal
6
@ Billy ONeal: Bagaimana jika saya melakukan implementasi file header seperti STL atau boost.
Martin York
6
@ Billy ONeal: Tidak. Ini masalah desain yang bagus, bukan pendapat. Menempatkannya di namespace tidak melindunginya dari penggunaan. Sekarang bagian dari API publik yang perlu dipertahankan untuk selamanya.
Martin York
21
@Billy ONeal: Ini melindunginya dari penggunaan yang tidak disengaja. Ini juga mendokumentasikan fakta bahwa itu pribadi dan tidak boleh digunakan (tidak dapat digunakan kecuali jika Anda melakukan sesuatu yang bodoh). Dengan demikian Anda tidak perlu mendukungnya. Menempatkannya di namespace menjadikannya bagian dari API publik (sesuatu yang Anda lewatkan dalam percakapan ini. API Publik berarti Anda harus mendukungnya).
Martin York
10
@Billy ONeal: Kelas bersarang memiliki beberapa keunggulan dibandingkan ruang nama bersarang: Anda tidak bisa membuat instance namespace, tetapi Anda bisa membuat instance kelas. Mengenai detailkonvensi: Alih-alih bergantung pada konvensi semacam itu yang perlu diingat sendiri, lebih baik bergantung pada kompiler yang melacaknya untuk Anda.
SasQ
142

Kelas bertingkat sama seperti kelas reguler, tetapi:

  • mereka memiliki batasan akses tambahan (seperti semua definisi di dalam definisi kelas lakukan),
  • mereka tidak mencemari namespace yang diberikan , misalnya namespace global. Jika Anda merasa bahwa kelas B terhubung sangat dalam ke kelas A, tetapi objek A dan B belum tentu terkait, maka Anda mungkin ingin kelas B hanya dapat diakses melalui pelingkupan kelas A (itu akan disebut sebagai A ::Kelas).

Beberapa contoh:

Kelas bersarang di depan umum untuk memasukkannya ke dalam lingkup kelas yang relevan


Asumsikan Anda ingin memiliki kelas SomeSpecificCollectionyang akan mengagregasi objek kelas Element. Anda kemudian dapat:

  1. mendeklarasikan dua kelas: SomeSpecificCollectiondan Element- buruk, karena nama "Elemen" cukup umum untuk menyebabkan kemungkinan bentrokan nama

  2. mengenalkan namespace someSpecificCollectiondan mendeklarasikan kelas someSpecificCollection::Collectiondan someSpecificCollection::Element. Tidak ada risiko bentrokan nama, tetapi bisakah itu mendapatkan lebih banyak kata-kata?

  3. mendeklarasikan dua kelas global SomeSpecificCollectiondan SomeSpecificCollectionElement- yang memiliki kelemahan kecil, tetapi mungkin OK.

  4. mendeklarasikan kelas SomeSpecificCollectiondan kelas global Elementsebagai kelas bersarangnya. Kemudian:

    • Anda tidak mengambil risiko bentrokan nama apa pun karena Elemen tidak ada dalam ruang nama global,
    • dalam implementasi SomeSpecificCollectionAnda merujuk ke adil Element, dan di tempat lain sebagai SomeSpecificCollection::Element- yang terlihat + - sama dengan 3., tetapi lebih jelas
    • itu menjadi sederhana bahwa itu "elemen koleksi tertentu", bukan "elemen koleksi tertentu"
    • terlihat SomeSpecificCollectionjuga kelas.

Menurut pendapat saya, varian terakhir pasti yang paling intuitif dan karenanya desain terbaik.

Biarkan saya tekankan - Ini bukan perbedaan besar dari membuat dua kelas global dengan lebih banyak nama bertele-tele. Itu hanya detail kecil, tapi itu membuat kode lebih jelas.

Memperkenalkan lingkup lain di dalam lingkup kelas


Ini sangat berguna untuk memperkenalkan typedef atau enum. Saya hanya akan memposting contoh kode di sini:

class Product {
public:
    enum ProductType {
        FANCY, AWESOME, USEFUL
    };
    enum ProductBoxType {
        BOX, BAG, CRATE
    };
    Product(ProductType t, ProductBoxType b, String name);

    // the rest of the class: fields, methods
};

Satu kemudian akan memanggil:

Product p(Product::FANCY, Product::BOX);

Tetapi ketika melihat proposal penyelesaian kode untuk Product::, seseorang akan sering mendapatkan semua nilai enum yang mungkin (KOTAK, FANCY, CRATE) terdaftar dan mudah untuk membuat kesalahan di sini (C ++ 0x jenis enum yang sangat diketik dari penyelesaian itu, tapi tidak apa-apa ).

Tetapi jika Anda memperkenalkan ruang lingkup tambahan untuk enum yang menggunakan kelas bersarang, hal-hal bisa terlihat seperti:

class Product {
public:
    struct ProductType {
        enum Enum { FANCY, AWESOME, USEFUL };
    };
    struct ProductBoxType {
        enum Enum { BOX, BAG, CRATE };
    };
    Product(ProductType::Enum t, ProductBoxType::Enum b, String name);

    // the rest of the class: fields, methods
};

Maka panggilan itu terlihat seperti:

Product p(Product::ProductType::FANCY, Product::ProductBoxType::BOX);

Kemudian dengan mengetikkan Product::ProductType::IDE, seseorang hanya akan mendapatkan enum dari cakupan yang diinginkan. Ini juga mengurangi risiko melakukan kesalahan.

Tentu saja ini mungkin tidak diperlukan untuk kelas kecil, tetapi jika seseorang memiliki banyak enum, maka itu membuat segalanya lebih mudah bagi pemrogram klien.

Dengan cara yang sama, Anda bisa "mengatur" sejumlah besar typedef dalam sebuah template, jika Anda pernah membutuhkannya. Terkadang ini merupakan pola yang bermanfaat.

Ungkapan PIMPL


PIMPL (kependekan dari Pointer to IMPLementation) adalah ungkapan yang berguna untuk menghapus detail implementasi suatu kelas dari header. Ini mengurangi kebutuhan mengkompilasi ulang kelas tergantung pada header kelas 'setiap kali bagian "implementasi" dari header berubah.

Ini biasanya diimplementasikan menggunakan kelas bersarang:

Xh:

class X {
public:
    X();
    virtual ~X();
    void publicInterface();
    void publicInterface2();
private:
    struct Impl;
    std::unique_ptr<Impl> impl;
}

X.cpp:

#include "X.h"
#include <windows.h>

struct X::Impl {
    HWND hWnd; // this field is a part of the class, but no need to include windows.h in header
    // all private fields, methods go here

    void privateMethod(HWND wnd);
    void privateMethod();
};

X::X() : impl(new Impl()) {
    // ...
}

// and the rest of definitions go here

Ini sangat berguna jika definisi kelas penuh membutuhkan definisi jenis dari beberapa perpustakaan eksternal yang memiliki file header yang berat atau hanya jelek (ambil WinAPI). Jika Anda menggunakan PIMPL, maka Anda dapat menyertakan fungsionalitas khusus WinAPI hanya di .cppdan tidak pernah memasukkannya ke dalam .h.

Kos
sumber
3
struct Impl; std::auto_ptr<Impl> impl; Kesalahan ini dipopulerkan oleh Herb Sutter. Jangan gunakan auto_ptr pada tipe yang tidak lengkap, atau setidaknya lakukan tindakan pencegahan untuk menghindari kesalahan kode yang dihasilkan.
Gene Bushuyev
2
@ Billy ONeal: Sejauh yang saya ketahui Anda dapat mendeklarasikan auto_ptrtipe tidak lengkap di sebagian besar implementasi tetapi secara teknis itu adalah UB tidak seperti beberapa templat di C ++ 0x (misalnya unique_ptr) di mana telah dibuat eksplisit bahwa parameter templat mungkin tipe yang tidak lengkap dan di mana tepatnya tipe tersebut harus lengkap. (mis. penggunaan ~unique_ptr)
CB Bailey
2
@Billy ONeal: Dalam C ++ 03 17.4.6.3 [lib.res.on.functions] mengatakan "Secara khusus, efeknya tidak terdefinisi dalam kasus-kasus berikut: [...] jika tipe tidak lengkap digunakan sebagai argumen templat ketika membuat komponen template. " sedangkan dalam C ++ 0x dikatakan "jika tipe yang tidak lengkap digunakan sebagai argumen templat ketika membuat komponen templat, kecuali secara khusus diizinkan untuk komponen itu." dan kemudian (mis.): "Parameter templat Tdari unique_ptrmungkin tipe yang tidak lengkap."
CB Bailey
1
@MilesRout Itu terlalu umum. Tergantung pada apakah kode klien diizinkan untuk diwariskan. Aturan: Jika Anda yakin bahwa Anda tidak akan menghapus melalui pointer kelas dasar, maka virtual dtor sepenuhnya berlebihan.
Kos
2
@IsaacPascual aww, saya harus memperbarui itu sekarang yang kita miliki enum class.
Kos
21

Saya tidak menggunakan kelas bersarang banyak, tapi saya menggunakannya sekarang dan kemudian. Terutama ketika saya mendefinisikan beberapa tipe data, dan saya kemudian ingin mendefinisikan functor STL yang dirancang untuk tipe data itu.

Misalnya, pertimbangkan Fieldkelas generik yang memiliki nomor ID, kode tipe, dan nama bidang. Jika saya ingin mencari salah satu vectordari ini Fielddengan nomor ID atau nama, saya mungkin membuat functor untuk melakukannya:

class Field
{
public:
  unsigned id_;
  string name_;
  unsigned type_;

  class match : public std::unary_function<bool, Field>
  {
  public:
    match(const string& name) : name_(name), has_name_(true) {};
    match(unsigned id) : id_(id), has_id_(true) {};
    bool operator()(const Field& rhs) const
    {
      bool ret = true;
      if( ret && has_id_ ) ret = id_ == rhs.id_;
      if( ret && has_name_ ) ret = name_ == rhs.name_;
      return ret;
    };
    private:
      unsigned id_;
      bool has_id_;
      string name_;
      bool has_name_;
  };
};

Kemudian kode yang perlu dicari ini Fielddapat menggunakan matchcakupan dalam Fieldkelas itu sendiri:

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));
John Dibling
sumber
Terima kasih atas contoh dan komentar yang bagus meskipun saya tidak cukup sadar dengan fungsi STL. Saya perhatikan bahwa konstruktor yang cocok () bersifat publik. Saya menganggap konstruktor tidak harus selalu bersifat publik dalam hal ini tidak dapat dipakai di luar kelas.
berkacamata
1
@ Pengguna: Dalam kasus functor STL, konstruktor harus bersifat publik.
John Dibling
1
@ Billy: Saya masih belum melihat alasan konkret mengapa kelas bersarang itu buruk.
John Dibling
@ John: Semua pedoman gaya pengkodean menjadi masalah pendapat. Saya mencantumkan beberapa alasan dalam beberapa komentar di sekitar sini, yang semuanya (menurut saya) masuk akal. Tidak ada argumen "faktual" yang dapat dibuat selama kode itu valid dan tidak memunculkan perilaku yang tidak jelas. Namun, saya pikir contoh kode yang Anda letakkan di sini menunjukkan alasan besar mengapa saya menghindari kelas bersarang - yaitu bentrokan nama.
Billy ONeal
1
Tentu saja ada alasan teknis untuk memilih inline daripada makro !!
Miles Rout
14

Seseorang dapat menerapkan pola Builder dengan kelas bersarang . Terutama di C ++, secara pribadi saya merasa lebih bersih secara semantik. Sebagai contoh:

class Product{
    public:
        class Builder;
}
class Product::Builder {
    // Builder Implementation
}

Daripada:

class Product {}
class ProductBuilder {}
Yeo
sumber
Tentu, itu akan berhasil jika hanya ada satu bangunan tetapi akan menjadi buruk jika ada kebutuhan untuk memiliki banyak pembangun beton. Orang harus hati-hati membuat keputusan desain :)
irsis