Tại sao người ta sẽ sử dụng các lớp lồng nhau trong C ++?


188

Ai đó có thể vui lòng chỉ cho tôi một số tài nguyên tốt để hiểu và sử dụng các lớp lồng nhau không? Tôi có một số tài liệu như Nguyên tắc lập trình và những thứ như Trung tâm kiến ​​thức IBM này - Các lớp lồng nhau

Nhưng tôi vẫn gặp khó khăn trong việc hiểu mục đích của họ. Có thể ai đó hãy giúp tôi?


15
Lời khuyên của tôi cho các lớp lồng nhau trong C ++ chỉ đơn giản là không sử dụng các lớp lồng nhau.
Billy ONeal

7
Chúng chính xác như các lớp học thông thường ... ngoại trừ lồng nhau. Sử dụng chúng khi triển khai nội bộ của một lớp quá phức tạp đến mức nó có thể dễ dàng được mô hình hóa bởi một số lớp nhỏ hơn.
meagar

12
@Billy: Tại sao? Có vẻ quá rộng đối với tôi.
John Dibling

30
Tôi vẫn chưa thấy một cuộc tranh luận tại sao các lớp lồng nhau là xấu bởi bản chất của chúng.
John Dibling

7
@ 7vies: 1. vì đơn giản là không cần thiết - bạn có thể làm tương tự với các lớp được xác định bên ngoài, điều này làm giảm phạm vi của bất kỳ biến đã cho nào, đây là một điều tốt. 2. bởi vì bạn có thể làm mọi thứ mà các lớp lồng nhau có thể làm với typedef. 3. bởi vì họ thêm một mức thụt lề bổ sung trong một môi trường mà việc tránh các hàng dài đã khó 4. vì bạn đang khai báo hai đối tượng riêng biệt về mặt khái niệm trong một classkhai báo, v.v.
Billy ONeal

Câu trả lời:


228

Các lớp lồng nhau là mát mẻ để ẩn chi tiết thực hiện.

Danh sách:

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

Ở đây tôi không muốn phơi bày Node vì những người khác có thể quyết định sử dụng lớp và điều đó sẽ cản trở tôi cập nhật lớp của mình vì mọi thứ được phơi bày là một phần của API công khai và phải được duy trì mãi mãi . Bằng cách đặt lớp riêng tư, tôi không chỉ che giấu việc triển khai mà tôi còn nói đây là của tôi và tôi có thể thay đổi nó bất cứ lúc nào để bạn không thể sử dụng nó.

Nhìn vào std::listhoặc std::maptất cả chúng đều chứa các lớp ẩn (hoặc chúng?). Vấn đề là họ có thể hoặc không, nhưng vì việc triển khai là riêng tư và ẩn, những người xây dựng STL đã có thể cập nhật mã mà không ảnh hưởng đến cách bạn sử dụng mã hoặc để lại nhiều hành lý cũ đặt xung quanh STL vì họ cần để duy trì khả năng tương thích ngược với một số kẻ ngu ngốc đã quyết định họ muốn sử dụng lớp Node được ẩn bên trong list.


9
Nếu bạn đang làm điều này thì Nodekhông nên hiển thị trong tệp tiêu đề.
Billy ONeal

6
@Billy ONeal: Điều gì xảy ra nếu tôi đang thực hiện tệp tiêu đề như STL hoặc boost.
Martin York

5
@Billy ONeal: Không. Đó là vấn đề thiết kế tốt không phải ý kiến. Đặt nó trong một không gian tên không bảo vệ nó khỏi sử dụng. Bây giờ nó là một phần của API công cộng cần được duy trì vĩnh viễn.
Martin York

21
@Billy ONeal: Nó bảo vệ nó khỏi việc sử dụng ngẫu nhiên. Nó cũng ghi lại sự thật rằng nó là riêng tư và không nên được sử dụng (không thể được sử dụng trừ khi bạn làm điều gì đó ngu ngốc). Do đó, bạn không cần phải hỗ trợ nó. Đặt nó trong một không gian tên làm cho nó trở thành một phần của API công khai (thứ bạn vẫn thiếu trong cuộc trò chuyện này. API công cộng có nghĩa là bạn cần hỗ trợ nó).
Martin York

10
@Billy ONeal: Lớp lồng nhau có một số lợi thế so với không gian tên lồng nhau: Bạn không thể tạo các thể hiện của một không gian tên, nhưng bạn có thể tạo các thể hiện của một lớp. Theo detailquy ước: Thay vì phụ thuộc vào các quy ước như vậy, người ta cần ghi nhớ chính mình, tốt hơn là phụ thuộc vào trình biên dịch theo dõi chúng cho bạn.
SasQ

142

Các lớp lồng nhau giống như các lớp thông thường, nhưng:

  • họ có hạn chế truy cập bổ sung (như tất cả các định nghĩa trong định nghĩa lớp làm),
  • họ không làm ô nhiễm không gian tên đã cho , ví dụ như không gian tên toàn cầu. Nếu bạn cảm thấy rằng lớp B được kết nối sâu sắc với lớp A, nhưng các đối tượng của A và B không nhất thiết phải liên quan với nhau, thì bạn có thể muốn lớp B chỉ có thể truy cập được thông qua phạm vi lớp A (nó sẽ được gọi là A ::Lớp học).

Vài ví dụ:

Lớp lồng nhau công khai để đặt nó trong một phạm vi của lớp có liên quan


Giả sử bạn muốn có một lớp SomeSpecificCollectionsẽ tổng hợp các đối tượng của lớp Element. Sau đó, bạn có thể:

  1. khai báo hai lớp: SomeSpecificCollectionElement- xấu, vì tên "Element" đủ chung để gây ra xung đột tên có thể

  2. giới thiệu một không gian tên someSpecificCollectionvà khai báo các lớp someSpecificCollection::CollectionsomeSpecificCollection::Element. Không có nguy cơ đụng độ tên, nhưng nó có thể nhận được nhiều dài dòng hơn không?

  3. khai báo hai lớp toàn cầu SomeSpecificCollectionSomeSpecificCollectionElement- có nhược điểm nhỏ, nhưng có lẽ là ổn.

  4. tuyên bố lớp toàn cầu SomeSpecificCollectionvà lớp Elementlà lớp lồng nhau của nó. Sau đó:

    • bạn không có nguy cơ đụng độ tên vì Element không có trong không gian tên toàn cầu,
    • trong quá trình thực hiện, SomeSpecificCollectionbạn chỉ nói đến Element, và mọi nơi khác như SomeSpecificCollection::Element- trông + - giống như 3., nhưng rõ ràng hơn
    • thật đơn giản rằng đó là "một yếu tố của một bộ sưu tập cụ thể", chứ không phải "một yếu tố cụ thể của một bộ sưu tập"
    • nó có thể nhìn thấy đó SomeSpecificCollectioncũng là một lớp

Theo tôi, biến thể cuối cùng chắc chắn là thiết kế trực quan nhất và do đó tốt nhất.

Hãy để tôi nhấn mạnh - Đó không phải là một sự khác biệt lớn so với việc tạo ra hai lớp toàn cầu với nhiều tên dài dòng hơn. Nó chỉ là một chi tiết nhỏ, nhưng imho nó làm cho mã rõ ràng hơn.

Giới thiệu một phạm vi khác trong phạm vi lớp


Điều này đặc biệt hữu ích để giới thiệu typedefs hoặc enums. Tôi sẽ chỉ đăng một ví dụ mã ở đây:

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

Một người sau đó sẽ gọi:

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

Nhưng khi xem xét các đề xuất hoàn thành mã Product::, người ta thường sẽ nhận được tất cả các giá trị enum có thể (BOX, FANCY, CRATE) được liệt kê và rất dễ mắc lỗi ở đây (C ++ 0x được đánh máy mạnh mẽ để giải quyết vấn đề đó, nhưng đừng bận tâm ).

Nhưng nếu bạn giới thiệu phạm vi bổ sung cho những enum sử dụng các lớp lồng nhau, mọi thứ có thể trông như sau:

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

Sau đó, cuộc gọi trông như:

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

Sau đó, bằng cách gõ Product::ProductType::vào một IDE, người ta sẽ chỉ nhận được các enum từ phạm vi mong muốn được đề xuất. Điều này cũng làm giảm nguy cơ mắc lỗi.

Tất nhiên điều này có thể không cần thiết cho các lớp nhỏ, nhưng nếu một người có nhiều enum, thì nó sẽ giúp mọi thứ dễ dàng hơn cho các lập trình viên máy khách.

Theo cách tương tự, bạn có thể "tổ chức" một loạt các typedefs trong một mẫu, nếu bạn có nhu cầu. Đôi khi nó là một mẫu hữu ích.

Thành ngữ PIMPL


PIMPL (viết tắt của Con trỏ đến IMPLementation) là một thành ngữ hữu ích để loại bỏ các chi tiết triển khai của một lớp khỏi tiêu đề. Điều này làm giảm nhu cầu biên dịch lại các lớp tùy thuộc vào tiêu đề của lớp bất cứ khi nào phần "triển khai" của tiêu đề thay đổi.

Nó thường được thực hiện bằng cách sử dụng một lớp lồng nhau:

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

Điều này đặc biệt hữu ích nếu định nghĩa lớp đầy đủ cần định nghĩa về các loại từ một thư viện bên ngoài có tệp tiêu đề nặng hoặc chỉ xấu (lấy WinAPI). Nếu bạn sử dụng PIMPL, thì bạn có thể chỉ bao gồm bất kỳ chức năng cụ thể nào của WinAPI .cppvà không bao giờ bao gồm nó .h.


3
struct Impl; std::auto_ptr<Impl> impl; Lỗi này đã được phổ biến bởi Herb Sutter. Không sử dụng auto_ptr trên các loại không đầy đủ hoặc ít nhất là thực hiện các biện pháp phòng ngừa để tránh tạo mã sai.
Gene Bushuyev

2
@Billy ONeal: Theo như tôi biết, bạn có thể khai báo một auto_ptrloại không hoàn chỉnh trong hầu hết các triển khai nhưng về mặt kỹ thuật, nó không giống như một số mẫu trong C ++ 0x (ví dụ unique_ptr) trong đó có thể nói rõ rằng tham số mẫu có thể là một loại không đầy đủ và nơi chính xác loại phải được hoàn thành. (ví dụ: sử dụng ~unique_ptr)
CB Bailey

2
@Billy ONeal: Trong C ++ 03 17.4.6.3 [lib.res.on.fifts] nói "Đặc biệt, các hiệu ứng không được xác định trong các trường hợp sau: [...] nếu một loại không hoàn chỉnh được sử dụng làm đối số mẫu khi khởi tạo một thành phần mẫu. " trong khi đó trong C ++ 0x, nó nói "nếu một loại không hoàn chỉnh được sử dụng làm đối số mẫu khi khởi tạo một thành phần mẫu, trừ khi được phép cụ thể cho thành phần đó." và sau này (ví dụ): "Tham số mẫu Tcủa unique_ptrcó thể là loại không đầy đủ."
CB Bailey

1
@MilesRout Đó là cách quá chung chung. Phụ thuộc vào việc mã khách hàng có được phép kế thừa hay không. Quy tắc: Nếu bạn chắc chắn rằng bạn sẽ không xóa qua một con trỏ lớp cơ sở, thì dtor ảo là hoàn toàn dư thừa.
Kos

2
@IsaacPascual aww, tôi nên cập nhật ngay bây giờ mà chúng ta có enum class.
Kos

21

Tôi không sử dụng các lớp lồng nhau nhiều, nhưng tôi sử dụng chúng ngay bây giờ và sau đó. Đặc biệt là khi tôi xác định một số loại dữ liệu và sau đó tôi muốn xác định một functor STL được thiết kế cho loại dữ liệu đó.

Ví dụ, hãy xem xét một Fieldlớp chung có số ID, mã loại và tên trường. Nếu tôi muốn tìm kiếm một vectortrong số này Fieldbằng số ID hoặc tên, tôi có thể xây dựng một functor để làm như vậy:

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

Sau đó, mã cần tìm kiếm các Fields này có thể sử dụng matchphạm vi trong Fieldchính lớp đó:

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

Cảm ơn bạn về ví dụ và nhận xét tuyệt vời mặc dù tôi không biết nhiều về chức năng STL. Tôi nhận thấy rằng các hàm tạo trong match () là công khai. Tôi cho rằng các nhà xây dựng không cần phải luôn luôn công khai trong trường hợp không thể khởi tạo bên ngoài lớp.
bespectacled

1
@user: Trong trường hợp của functor STL, hàm tạo cần phải được công khai.
John Dibling

1
@Billy: Tôi vẫn chưa thấy bất kỳ lý do cụ thể tại sao các lớp lồng nhau là xấu.
John Dibling

@ John: Tất cả các hướng dẫn về phong cách mã hóa đều đi vào vấn đề quan điểm. Tôi liệt kê một số lý do trong một số ý kiến ​​xung quanh đây, tất cả trong số đó (theo ý kiến ​​của tôi) là hợp lý. Không có đối số "thực tế" có thể được thực hiện miễn là mã hợp lệ và không có hành vi không xác định. Tuy nhiên, tôi nghĩ rằng ví dụ mã bạn đặt ở đây chỉ ra một lý do lớn tại sao tôi tránh các lớp lồng nhau - cụ thể là xung đột tên.
Billy ONeal

1
Tất nhiên có những lý do kỹ thuật để thích nội tuyến cho macro !!
Miles Rout

14

Người ta có thể thực hiện một mô hình Builder với lớp lồng nhau . Đặc biệt là trong C ++, cá nhân tôi thấy nó sạch hơn về mặt ngữ nghĩa. Ví dụ:

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

Thay vì:

class Product {}
class ProductBuilder {}

Chắc chắn, nó sẽ hoạt động nếu chỉ có một bản dựng nhưng sẽ trở nên khó chịu nếu cần phải có nhiều người xây dựng cụ thể.
Mọi người
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.