Neden C ++ 'da iç içe sınıflar kullanılır?


188

Birisi beni iç içe dersleri anlamak ve kullanmak için güzel kaynaklara yönlendirebilir mi? Programlama Prensipleri ve IBM Bilgi Merkezi - İç İçe Sınıflar gibi bazı materyallerim var

Ama hala amaçlarını anlamakta güçlük çekiyorum. Birisi bana yardım edebilir mi?


15
C ++ 'da yuvalanmış sınıflar için tavsiyem, sadece yuvalanmış sınıfları kullanmamaktır.
Billy ONeal

7
Tam olarak normal sınıflara benziyorlar ... iç içe olanlar hariç. Bunları bir sınıfın dahili uygulaması o kadar karmaşık olduğunda, daha küçük sınıflar tarafından en kolay şekilde modellenebilecek şekilde kullanın.
meagar

12
@Billy: Neden? Bana çok geniş gözüküyor.
John Dibling

30
İç içe sınıfların neden doğası gereği kötü olduğunu iddia etmedim.
John Dibling

7
@ 7vies: 1. çünkü sadece gerekli değildir - aynı şeyi, herhangi bir değişkenin kapsamını azaltan harici olarak tanımlanmış sınıflarla yapabilirsiniz, bu iyi bir şeydir. 2. çünkü iç içe sınıfların yapabileceği her şeyi yapabilirsiniz typedef. 3. Tek bir iki kavramsal ayrı nesneler ilan çünkü uzun satırları kaçınarak zaten 4. zor olmadığı bir ortamda girinti ek bir düzey eklemek çünkü classvb beyanı,
Billy Oneal

Yanıtlar:


229

Yuvalanmış sınıflar uygulama ayrıntılarını gizlemek için iyidir.

Liste:

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

Burada düğümü ifşa etmek istemiyorum çünkü diğer insanlar sınıfı kullanmaya karar verebilir ve maruz kalan herhangi bir şey genel API'nin bir parçası olduğu ve sonsuza kadar sürdürülmesi gerektiği için sınıfımı güncellememi engeller . Sınıfı özel yaparak, sadece benim olduğunu söyleyeceğim uygulamayı gizlemekle kalmam, bunu istediğiniz zaman değiştirebilirim, böylece kullanamazsınız.

Bakın std::listya std::mapda hepsi gizli sınıflar içeriyor (ya da var mı?). Mesele şu ki olabilir ya da olmayabilir, ancak uygulama özel ve gizli olduğu için STL'nin oluşturucuları, kodu nasıl kullandığınızı etkilemeden veya ihtiyaç duydukları için çok sayıda eski bagajı bırakmadan kodu güncelleyebildiler. içeride gizli olan Node sınıfını kullanmak istediklerine karar veren bazı aptallarla geriye dönük uyumluluk sağlamak list.


9
Bunu yapıyorsanız Node, başlık dosyasında hiç gösterilmemelidir.
Billy ONeal

6
@Billy ONeal: STL veya boost gibi bir başlık dosyası uygulaması yapıyorsam ne olur?
Martin York

6
@Billy ONeal: Hayır. Bu iyi bir tasarım meselesi değil. Bir ad alanına koymak, onu kullanımdan korumaz. Artık kalıcı API için sürdürülmesi gereken genel API'nin bir parçasıdır.
Martin York

21
@Billy ONeal: Yanlışlıkla kullanımdan korur. Ayrıca özel ve kullanılmaması gerektiği gerçeğini de belgelemektedir (aptalca bir şey yapmadıkça kullanılamaz). Böylece onu desteklemenize gerek yoktur. Bir ad alanına yerleştirmek onu genel API'nın bir parçası yapar (bu görüşmede kaçırmaya devam ettiğiniz bir şey. Genel API bunu desteklemeniz gerektiği anlamına gelir).
Martin York

10
@Billy ONeal: İç içe sınıfın iç içe geçmiş ad alanına göre bir avantajı vardır: Bir ad alanının örneklerini oluşturamazsınız, ancak bir sınıfın örneklerini oluşturabilirsiniz. detailSözleşmeye gelince : Bunun yerine, bu tür sözleşmelere bağlı olarak, kişinin aklında tutması gerekir, bunları sizin için takip eden derleyiciye güvenmek daha iyidir.
SasQ

142

Yuvalanmış sınıflar normal sınıflar gibidir, ancak:

  • ek erişim kısıtlamaları vardır (bir sınıf tanımındaki tüm tanımlar gibi),
  • Onlar verilen ad kirletmeyen genel ad alanını örneğin,. B sınıfının A sınıfına o kadar derinden bağlı olduğunu düşünüyorsanız, ancak A ve B nesnelerinin ille de ilişkili olmadığını düşünüyorsanız, B sınıfının yalnızca A sınıfının kapsamını kullanarak erişilebilir olmasını isteyebilirsiniz (A olarak adlandırılır. ::Sınıf).

Bazı örnekler:

İlgili sınıfın kapsamına koymak için genel olarak sınıflandırma


Sınıf SomeSpecificCollectionnesnelerini toplayacak bir sınıfa sahip olmak istediğinizi varsayalım Element. Daha sonra aşağıdakilerden birini yapabilirsiniz:

  1. iki sınıf bildirin: SomeSpecificCollectionve Element- kötü, çünkü "Element" ismi olası bir isim çatışmasına neden olacak kadar genel

  2. bir ad alanı tanıtmak someSpecificCollectionve sınıfları bildirmek someSpecificCollection::Collectionve someSpecificCollection::Element. İsim çatışması riski yok, ama daha ayrıntılı olabilir mi?

  3. iki küresel sınıf ilan SomeSpecificCollectionve SomeSpecificCollectionElement- küçük dezavantajları vardır, ama muhtemelen sorun yok.

  4. global sınıfı SomeSpecificCollectionve sınıfı Elementiç içe sınıfı olarak ilan eder. Sonra:

    • Öğe genel ad alanında olmadığı için hiçbir adın çakışması riskini almazsınız,
    • uygulamanızda SomeSpecificCollectionadil Elementve başka her yerdeSomeSpecificCollection::Element , 3 ile aynı görünen, + görünen, ancak daha açık olan
    • "koleksiyonun belirli bir öğesi" değil, "belirli bir koleksiyonun öğesi" olması çok basit
    • o SomeSpecificCollectionda bir sınıftır.

Bence son varyant kesinlikle en sezgisel ve dolayısıyla en iyi tasarım.

Vurgulayayım - Daha ayrıntılı isimlerle iki küresel sınıf yapmaktan büyük bir fark yok. Bu sadece küçük bir detay, ama imho kodu daha net hale getiriyor.

Sınıf kapsamı içinde başka bir kapsamın tanıtımı


Bu özellikle typedefs veya enum'ları tanıtmak için kullanışlıdır. Buraya bir kod örneği göndereceğim:

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

Biri arayacak:

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

Ancak, kod tamamlama önerilerine bakarken Product:: genellikle listelenen tüm olası numaralandırma değerlerini (BOX, FANCY, CRATE) alır ve burada bir hata yapmak kolaydır (C ++ 0x'in güçlü yazılan numaralandırmalar bunu çözer, ama boş verin) ).

Ancak, yuvalanmış sınıfları kullanan bu numaralandırmalar için ek kapsam eklerseniz, işler şöyle görünebilir:

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

Sonra çağrı şöyle görünür:

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

Sonra yazarak Product::ProductType:: bir IDE , yalnızca önerilen kapsamdan numaralandırmalar alınacaktır. Bu aynı zamanda hata yapma riskini de azaltır.

Tabii ki bu, küçük sınıflar için gerekli olmayabilir, ancak çok sayıda numaralandırma varsa, istemci programcıları için işleri kolaylaştırır.

Aynı şekilde, ihtiyaç duymanız halinde, bir şablonda çok sayıda typedef'i "organize edebilirsiniz". Bazen faydalı bir model.

PIMPL deyimi


PIMPL (Pointer'dan IMPLementation'a kısaltma), bir sınıfın uygulama ayrıntılarını başlıktan kaldırmak için yararlı bir deyimdir. Bu, başlığın "uygulama" kısmı her değiştiğinde sınıfın başlığına bağlı olarak sınıfların yeniden derlenmesi ihtiyacını azaltır.

Genellikle iç içe bir sınıf kullanılarak uygulanır:

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

Tam sınıf tanımının, ağır veya sadece çirkin bir üstbilgi dosyasına (WinAPI al) sahip bazı harici kitaplıklardan tanım gerektirmesi özellikle yararlıdır. PIMPL kullanıyorsanız, WinAPI'ye özgü işlevleri yalnızca içine ekleyebilir .cppve asla içine dahil edemezsiniz .h.


3
struct Impl; std::auto_ptr<Impl> impl; Bu hata Herb Sutter tarafından popüler hale getirildi. Auto_ptr komutunu eksik türlerde kullanmayın veya en azından yanlış kod oluşturulmasını önlemek için önlem alın.
Gene Bushuyev

2
@Billy ONeal: Bildiğim kadarıyla auto_ptrçoğu uygulamada eksik bir tür bildirebilirsiniz , ancak teknik unique_ptrolarak, şablon parametresinin açık olabileceği açık hale getirildiği C ++ 0x (örn. ) ' Deki bazı şablonlardan farklı olarak UB'dir . eksik bir tür ve tam olarak burada türün tam olması gerekir. (örn. kullanımı ~unique_ptr)
CB Bailey

2
@Billy ONeal: C ++ 03 17.4.6.3'te [lib.res.on.functions] "Özellikle, aşağıdaki durumlarda efektler tanımsızdır: [...] şablon argümanı olarak eksik bir tür kullanılıyorsa bir şablon bileşenini başlatırken. " oysa C ++ 0x'de "bileşen için özel olarak izin verilmedikçe, bir şablon bileşenini başlatırken şablon türü olarak eksik bir tür kullanılıyorsa" der. ve daha sonra (örneğin) "şablon parametre Tarasında unique_ptrtamamlanmamış türü olabilir."
CB Bailey

1
@MilesRout Bu çok genel. İstemci kodunun devralmasına izin verilip verilmediğine bağlıdır. Kural: Temel sınıf işaretçisi ile silmeyeceğinizden eminseniz, sanal dtor tamamen yedeklidir.
Kos

2
@IsaacPascual aww, şimdi sahip olduğumuzu güncellemeliyim enum class.
Kos

21

İç içe dersleri çok fazla kullanmıyorum ama arada sırada kullanıyorum. Özellikle bir tür veri türü tanımladığımda ve o veri türü için tasarlanmış bir STL işlevi tanımlamak istediğimde.

Örneğin, Fieldkimlik numarası, tür kodu ve alan adı olan genel bir sınıf düşünün . vectorBu Fields birini kimlik numarası veya ad ile aramak istiyorsanız , bunu yapmak için bir işlev oluşturabilir:

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

Sonra bu Fields aramak için gereken kod sınıf matchiçinde Fieldkendi kapsamı kullanabilirsiniz :

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));

Harika örnek ve yorumlar için teşekkür ederim, ancak STL işlevlerinden pek haberdar değilim. Match () içindeki kurucuların herkese açık olduğunu fark ettim. Yapıcıların her zaman halka açık olması gerekmediğini düşünüyorum, bu durumda sınıfın dışında örneklenemez.
gözlüklü

1
@ kullanıcı: Bir STL işlevi durumunda, kurucunun halka açık olması gerekir.
John Dibling

1
@Billy: Hala iç içe derslerin neden kötü olduğunu gösteren herhangi bir somut akıl görmedim.
John Dibling

@John: Tüm kodlama stili yönergeleri bir görüş meselesi haline gelir. Burada (benim görüşüme göre) makul çeşitli yorumlar çeşitli nedenleri listelenmiştir. Kod geçerli olduğu ve tanımlanmamış davranış gerektirmediği sürece yapılabilecek hiçbir "olgusal" argüman yoktur. Ancak, buraya koyduğunuz kod örneğinin iç içe sınıflardan kaçınmamın büyük bir nedeni olduğunu düşünüyorum - yani isim çakışıyor.
Billy ONeal

1
Tabii ki satır içi makroları tercih etmek için teknik nedenler var !!
Miles Rout

14

Yuvalanmış sınıfla bir Builder kalıbı uygulanabilir . Özellikle C ++ 'da kişisel olarak anlamsal olarak daha temiz buluyorum. Örneğin:

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

Ziyade:

class Product {}
class ProductBuilder {}

Elbette, sadece bir yapı varsa işe yarayacak, ancak birden fazla beton üreticisine ihtiyaç duyulursa kötü olacak. Biri dikkatli bir şekilde tasarım kararları
almalı
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.