เหตุใดจึงใช้คลาสที่ซ้อนกันใน C ++


188

ใครช่วยชี้ให้ฉันดูแหล่งข้อมูลดีๆเพื่อทำความเข้าใจและใช้คลาสที่ซ้อนกันได้ ฉันมีเนื้อหาบางอย่างเช่นหลักการเขียนโปรแกรมและสิ่งต่าง ๆ เช่นIBM Knowledge Center - Nested Classes นี้

แต่ฉันยังคงมีปัญหาในการทำความเข้าใจวัตถุประสงค์ของพวกเขา มีคนช่วยฉันหน่อยได้ไหม


15
คำแนะนำของฉันสำหรับคลาสที่ซ้อนใน C ++ เป็นเพียงการไม่ใช้คลาสที่ซ้อนกัน
Billy ONeal

7
พวกมันเหมือนกับคลาสปกติ ... ยกเว้นซ้อนกัน ใช้เมื่อการใช้งานภายในของคลาสนั้นซับซ้อนมากจนสามารถสร้างโมเดลได้ง่ายโดยคลาสที่เล็กกว่าหลายคลาส
meagar

12
@Billy: ทำไม ดูเหมือนจะกว้างเกินไปสำหรับฉัน
John Dibling

30
ฉันยังไม่เห็นข้อโต้แย้งว่าทำไมคลาสที่ซ้อนกันไม่ดีตามธรรมชาติ
John Dibling

7
@ 7vies: 1. เพราะมันไม่จำเป็น - คุณสามารถทำเช่นเดียวกันกับคลาสที่กำหนดจากภายนอกซึ่งจะลดขอบเขตของตัวแปรที่กำหนดซึ่งเป็นสิ่งที่ดี 2. typedefทุกอย่างเพราะคุณสามารถจะเรียนซ้อนกันสามารถทำอะไรกับ 3. เนื่องจากพวกมันเพิ่มระดับการเยื้องเพิ่มเติมในสภาพแวดล้อมที่การหลีกเลี่ยงเส้นยาวนั้นเป็นเรื่องยากอยู่แล้ว 4. เพราะคุณกำลังประกาศวัตถุสองแนวคิดที่แยกจากกันในการclassประกาศเดียวฯลฯ
Billy ONeal

คำตอบ:


229

คลาสที่ซ้อนกันนั้นยอดเยี่ยมสำหรับการซ่อนรายละเอียดการใช้งาน

รายการ:

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

นี่ฉันไม่ต้องการที่จะเปิดเผยโหนดที่คนอื่น ๆ อาจตัดสินใจที่จะใช้ในชั้นเรียนและที่จะขัดขวางฉันจากการปรับปรุงชั้นเรียนของฉันเป็นสิ่งที่สัมผัสเป็นส่วนหนึ่งของประชาชน API และจะต้องได้รับการรักษาไปตลอดกาล ด้วยการทำให้ชั้นเรียนเป็นส่วนตัวฉันไม่เพียง แต่ซ่อนการนำไปใช้ฉันยังบอกว่านี่เป็นของฉันและฉันสามารถเปลี่ยนแปลงได้ตลอดเวลาเพื่อให้คุณไม่สามารถใช้งานได้

ดูstd::listหรือstd::mapพวกเขาทั้งหมดมีชั้นเรียนที่ซ่อนอยู่ (หรือพวกเขา?) ประเด็นก็คือพวกเขาอาจจะหรืออาจจะไม่ แต่เนื่องจากการใช้งานเป็นส่วนตัวและซ่อนผู้สร้างของ STL สามารถอัปเดตรหัสโดยไม่ส่งผลกระทบต่อวิธีที่คุณใช้รหัสหรือทิ้งสัมภาระเก่าจำนวนมากวางรอบ STL เพราะพวกเขาต้องการ listเพื่อรักษาความเข้ากันได้ย้อนหลังกับคนโง่บางคนที่ตัดสินใจที่พวกเขาต้องการที่จะใช้ระดับโหนดที่ถูกซ่อนอยู่ภายใน


9
หากคุณทำสิ่งนี้คุณไม่Nodeควรเปิดเผยในไฟล์ส่วนหัวเลย
Billy ONeal

6
@Billy ONeal: จะเกิดอะไรขึ้นถ้าฉันกำลังใช้งานไฟล์ส่วนหัวเช่น STL หรือเพิ่ม
Martin York

6
@Billy ONeal: ไม่มันเป็นเรื่องของการออกแบบที่ดีไม่ใช่ความคิดเห็น การวางไว้ในเนมสเปซไม่ได้ป้องกันการใช้งาน ตอนนี้เป็นส่วนหนึ่งของ API สาธารณะที่ต้องได้รับการดูแลเพื่อความยั่งยืน
Martin York

21
@Billy ONeal: ช่วยปกป้องมันจากการใช้โดยไม่ตั้งใจ มันเป็นเอกสารเกี่ยวกับความจริงที่ว่ามันเป็นแบบส่วนตัวและไม่ควรใช้ (ไม่สามารถใช้งานได้จนกว่าคุณจะทำอะไรที่โง่) ดังนั้นคุณไม่จำเป็นต้องสนับสนุนมัน การวางไว้ในเนมสเปซทำให้มันเป็นส่วนหนึ่งของ API สาธารณะ (สิ่งที่คุณขาดหายไปในการสนทนานี้ API สาธารณะหมายถึงคุณต้องให้การสนับสนุน)
Martin York

10
@Billy ONeal: คลาสที่ซ้อนกันมีข้อได้เปรียบเหนือเนมสเปซที่ซ้อนกัน: คุณไม่สามารถสร้างอินสแตนซ์ของเนมสเปซ แต่คุณสามารถสร้างอินสแตนซ์ของคลาสได้ ตามdetailอนุสัญญา: แทนที่จะขึ้นอยู่กับอนุสัญญาดังกล่าวเราจำเป็นต้องระลึกถึงตัวคุณเองมันจะดีกว่าหากคุณต้องพึ่งพาคอมไพเลอร์ที่คอยติดตามพวกเขาแทนคุณ
SasQ

142

คลาสที่ซ้อนกันนั้นเหมือนกับคลาสปกติ แต่:

  • พวกเขามีข้อ จำกัด การเข้าถึงเพิ่มเติม (ตามคำจำกัดความทั้งหมดภายในคำจำกัดความของคลาส)
  • พวกเขาไม่สร้างมลภาวะให้กับ namespace ที่กำหนดเช่นเนมสเปซส่วนกลาง หากคุณรู้สึกว่าคลาส B นั้นเชื่อมโยงอย่างลึกซึ้งกับคลาส A แต่วัตถุของ A และ B นั้นไม่จำเป็นต้องเกี่ยวข้องกันดังนั้นคุณอาจต้องการให้คลาส B เข้าถึงได้ผ่านการกำหนดขอบเขตคลาส A เท่านั้น (มันจะเรียกว่า A :: ชั้น)

ตัวอย่างบางส่วน:

คลาสซ้อนแบบพับลิกเพื่อวางไว้ในขอบเขตของคลาสที่เกี่ยวข้อง


สมมติว่าคุณต้องการที่จะได้เรียนซึ่งจะรวบรวมวัตถุของคลาสSomeSpecificCollection Elementจากนั้นคุณสามารถ:

  1. ประกาศสองคลาส: SomeSpecificCollectionและElement- ไม่ดีเนื่องจากชื่อ "องค์ประกอบ" เป็นชื่อทั่วไปเพียงพอที่จะทำให้เกิดการปะทะกันของชื่อที่เป็นไปได้

  2. แนะนำ namespace someSpecificCollectionและประกาศการเรียนและsomeSpecificCollection::Collection someSpecificCollection::Elementไม่มีความเสี่ยงจากการปะทะกันของชื่อ แต่จะได้รับรายละเอียดเพิ่มเติมอีกหรือไม่

  3. ประกาศคลาสระดับโลกสองคลาสSomeSpecificCollectionและSomeSpecificCollectionElement- ซึ่งมีข้อเสียเล็กน้อย แต่อาจโอเค

  4. ประกาศคลาสระดับโลกSomeSpecificCollectionและคลาสElementเป็นคลาสที่ซ้อนกัน แล้ว:

    • คุณไม่เสี่ยงต่อการปะทะกันของชื่อใด ๆ เนื่องจากองค์ประกอบไม่ได้อยู่ในเนมสเปซส่วนกลาง
    • ในการใช้งานของSomeSpecificCollectionคุณหมายถึงเพียงElementและทุกที่อื่น ๆSomeSpecificCollection::Element- ซึ่งมีลักษณะ + - เช่นเดียวกับ 3 แต่ชัดเจนยิ่งขึ้น
    • มันเรียบง่ายธรรมดาว่า "องค์ประกอบของคอลเลกชันเฉพาะ" ไม่ใช่ "องค์ประกอบเฉพาะของคอลเลกชัน"
    • มันสามารถมองเห็นได้ว่าSomeSpecificCollectionเป็นชั้นเรียนด้วย

ในความคิดของฉันตัวแปรสุดท้ายคือแน่นอนที่สุดใช้งานง่ายและดังนั้นจึงออกแบบที่ดีที่สุด

ให้ฉันเครียด - มันไม่แตกต่างกันมากจากการสร้างสองระดับโลกที่มีชื่อ verbose เพิ่มเติม มันเป็นรายละเอียดเล็ก ๆ น้อย ๆ แต่ imho ทำให้รหัสชัดเจนยิ่งขึ้น

แนะนำขอบเขตอื่นภายในขอบเขตของคลาส


สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับการแนะนำ typedefs หรือ enums ฉันจะโพสต์ตัวอย่างรหัสที่นี่:

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

หนึ่งแล้วจะโทร:

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

แต่เมื่อดูที่ข้อเสนอการทำโค้ดให้เสร็จสมบูรณ์Product::เรามักจะได้รับค่า enum ที่เป็นไปได้ทั้งหมด (BOX, FANCY, CRATE) และมันง่ายที่จะทำผิดพลาดได้ที่นี่ (C ++ 0x พิมพ์อย่างยิ่ง enums ชนิดของการแก้ปัญหา )

แต่ถ้าคุณแนะนำขอบเขตเพิ่มเติมสำหรับ enums เหล่านั้นโดยใช้คลาสที่ซ้อนกันสิ่งต่าง ๆ อาจมีลักษณะดังนี้:

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

จากนั้นการโทรจะมีลักษณะดังนี้:

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

จากนั้นโดยการพิมพ์Product::ProductType::ใน IDE หนึ่งจะได้รับเพียง enums จากขอบเขตที่ต้องการแนะนำ นอกจากนี้ยังช่วยลดความเสี่ยงในการทำผิดพลาด

หลักสูตรนี้อาจไม่จำเป็นสำหรับชั้นเรียนขนาดเล็ก แต่หากมีจำนวนมาก enums แล้วมันจะทำให้สิ่งที่ง่ายขึ้นสำหรับโปรแกรมเมอร์ไคลเอนต์

ในทำนองเดียวกันคุณสามารถ "จัดระเบียบ" กลุ่มข้อความขนาดใหญ่ในเทมเพลตหากคุณต้องการ มันเป็นรูปแบบที่มีประโยชน์บางครั้ง

PIMPL สำนวน


PIMPL (ย่อมาจาก Pointer to IMPLementation) เป็นสำนวนที่มีประโยชน์ในการลบรายละเอียดการใช้งานของคลาสออกจากส่วนหัว สิ่งนี้จะช่วยลดความต้องการในการคอมไพล์คลาสใหม่โดยขึ้นอยู่กับส่วนหัวของคลาสเมื่อใดก็ตามที่ส่วน "การใช้งาน" ของการเปลี่ยนแปลงส่วนหัว

มันมักจะนำมาใช้โดยใช้ชั้นซ้อนกัน:

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

สิ่งนี้มีประโยชน์อย่างยิ่งหากนิยามคลาสแบบเต็มต้องการนิยามชนิดจากไลบรารีภายนอกบางตัวที่มีไฟล์ส่วนหัวที่หนักหรือน่าเกลียด (รับ WinAPI) ถ้าคุณใช้ PIMPL แล้วคุณสามารถแนบใด ๆ การทำงาน WinAPI เฉพาะเฉพาะในและไม่เคยรวมไว้ใน.cpp.h


3
struct Impl; std::auto_ptr<Impl> impl; ข้อผิดพลาดนี้ได้รับความนิยมโดย Herb Sutter อย่าใช้ auto_ptr กับประเภทที่ไม่สมบูรณ์หรืออย่างน้อยต้องระวังเพื่อหลีกเลี่ยงการสร้างรหัสผิด
Gene Bushuyev

2
@Billy ONeal: เท่าที่ฉันทราบคุณสามารถประกาศauto_ptrประเภทที่ไม่สมบูรณ์ในการใช้งานส่วนใหญ่ แต่ในทางเทคนิคแล้วมันเป็น UB ซึ่งแตกต่างจากบางส่วนของแม่แบบใน C ++ 0x (เช่นunique_ptr) ซึ่งมีการทำอย่างชัดเจนว่าพารามิเตอร์แม่แบบอาจ ประเภทที่ไม่สมบูรณ์และประเภทที่จะต้องสมบูรณ์ (เช่นการใช้~unique_ptr)
CB Bailey

2
@Billy ONeal: ใน C ++ 03 17.4.6.3 [lib.res.on.functions] กล่าวว่า "โดยเฉพาะเอฟเฟกต์จะไม่ได้กำหนดในกรณีต่อไปนี้: [... ] ถ้าใช้ประเภทที่ไม่สมบูรณ์เป็นอาร์กิวเมนต์เท็มเพลต เมื่อสร้างอินสแตนซ์ขององค์ประกอบเท็มเพลต " ในขณะที่ใน C ++ 0x มีข้อความระบุว่า "หากใช้ประเภทที่ไม่สมบูรณ์เป็นอาร์กิวเมนต์เท็มเพลตเมื่อสร้างอินสแตนซ์ขององค์ประกอบเทมเพลตยกเว้นว่าได้รับอนุญาตเป็นพิเศษสำหรับส่วนประกอบนั้น" และใหม่กว่า (เช่น): "พารามิเตอร์เทมเพลตTของunique_ptrอาจเป็นประเภทที่ไม่สมบูรณ์"
CB Bailey

1
@MilesRout นั่นเป็นวิธีทั่วไปเกินไป ขึ้นอยู่กับว่ารหัสลูกค้าได้รับอนุญาตให้สืบทอด กฎ: ถ้าคุณมั่นใจว่าคุณจะไม่ลบตัวชี้ผ่านคลาสพื้นฐานแสดงว่า dtor เสมือนนั้นซ้ำซ้อนอย่างสมบูรณ์
Kos

2
@IsaacPascual Aww enum classฉันควรจะอัปเดตว่าตอนนี้ที่เรามี
Kos

21

ฉันไม่ได้ใช้คลาสที่ซ้อนกันมาก แต่ฉันจะใช้พวกเขาตอนนี้แล้ว โดยเฉพาะอย่างยิ่งเมื่อฉันกำหนดประเภทข้อมูลบางประเภทและจากนั้นฉันต้องการกำหนดฟังก์ชั่น STL ที่ออกแบบมาสำหรับประเภทข้อมูลนั้น

ตัวอย่างเช่นพิจารณาFieldคลาสทั่วไปที่มีหมายเลข ID รหัสประเภทและชื่อเขตข้อมูล หากฉันต้องการค้นหาสิ่งvectorเหล่านี้Fieldด้วยหมายเลข ID หรือชื่อฉันอาจสร้าง functor ให้ทำ:

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

จากนั้นโค้ดที่ต้องการค้นหาเหล่านี้Fieldสามารถใช้การmatchกำหนดขอบเขตภายในFieldคลาสเอง:

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

ขอบคุณสำหรับตัวอย่างที่ดีและความคิดเห็นแม้ว่าฉันจะไม่ได้ตระหนักถึงฟังก์ชั่น STL ฉันสังเกตว่าตัวสร้างใน match () เป็นแบบสาธารณะ ฉันคิดว่าผู้สร้างไม่จำเป็นต้องเปิดเผยต่อสาธารณะเสมอในกรณีที่ไม่สามารถสร้างอินสแตนซ์นอกห้องเรียนได้
bespectacled

1
@user: ในกรณีของ STL functor ผู้สร้างไม่จำเป็นต้องเปิดเผยต่อสาธารณะ
John Dibling

1
@Billy: ฉันยังไม่ได้เห็นเหตุผลที่เป็นรูปธรรมว่าทำไมคลาสที่ซ้อนกันไม่ดี
John Dibling

@ จอห์น: แนวทางการเขียนโค้ดสไตล์ทั้งหมดมาลงในเรื่องของความคิดเห็น ฉันแสดงเหตุผลหลายประการในความคิดเห็นหลาย ๆ จุดซึ่งทั้งหมดนี้ (ในความเห็นของฉัน) นั้นสมเหตุสมผล ไม่มีอาร์กิวเมนต์ "จริง" ที่สามารถสร้างได้ตราบใดที่รหัสนั้นถูกต้องและไม่เรียกใช้พฤติกรรมที่ไม่ได้กำหนด อย่างไรก็ตามฉันคิดว่าตัวอย่างรหัสที่คุณใส่ไว้ที่นี่ชี้ให้เห็นถึงเหตุผลใหญ่ว่าทำไมฉันจึงหลีกเลี่ยงคลาสที่ซ้อนกัน - นั่นคือชื่อการปะทะกัน
Billy ONeal

1
แน่นอนว่ามีเหตุผลทางเทคนิคที่ต้องการอินไลน์ให้กับมาโคร !!
เส้นทาง Miles Rout

14

หนึ่งสามารถใช้รูปแบบการสร้างกับชั้นที่ซ้อนกัน โดยเฉพาะอย่างยิ่งใน C ++ โดยส่วนตัวแล้วฉันคิดว่ามันสะอาดกว่า ตัวอย่างเช่น:

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

ค่อนข้างมากกว่า:

class Product {}
class ProductBuilder {}

แน่นอนว่าจะใช้งานได้หากมีเพียงงานสร้างเดียว แต่จะน่ารังเกียจหากจำเป็นต้องมีผู้สร้างหลายคน หนึ่งควรจะระมัดระวังในการตัดสินใจการออกแบบ :)
irsis
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.