为什么要在C++中使用嵌套类?

zen*_*gal 180 c++ nested inner-classes

有人可以指点我一些很好的资源来理解和使用嵌套类吗?我有一些材料,如编程原理和类似IBM知识中心 - 嵌套类的东西

但我仍然无法理解他们的目的.有人可以帮帮我吗?

Mar*_*ork 215

嵌套类很容易隐藏实现细节.

列表:

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

在这里,我不想暴露Node,因为其他人可能决定使用该类,这会阻碍我更新我的类,因为暴露的任何东西都是公共API的一部分,必须永远维护.通过使类私有,我不仅隐藏了实现,我也说这是我的,我可能随时更改它,所以你不能使用它.

看看std::list或者std::map它们都包含隐藏的类(或者它们是什么?).重点是它们可能会也可能不会,但是因为实现是私有的并且隐藏了STL的构建者能够在不影响您使用代码的方式的情况下更新代码,或者因为需要而在STL周围留下大量旧行李保持与某些傻瓜的向后兼容性,他们决定使用隐藏在其中的Node类list.

  • @Billy ONeal:它可以防止意外使用.它还记录了它是私有的,不应该使用的事实(除非你做一些愚蠢的事情,否则不能使用它).因此,您不需要支持它.将它放在命名空间中使它成为公共API的一部分(在这个对话中你一直缺少.公共API意味着你需要支持它). (20认同)
  • 如果你这样做,那么`Node`根本不应该暴露在头文件中. (9认同)
  • @Billy ONeal:嵌套类比嵌套命名空间有一些优势:您不能创建命名空间的实例,但可以创建类的实例.关于`detail`约定:取决于这些约定,需要记住自己,最好依赖于编译器来跟踪它们. (9认同)
  • @Billy ONeal:如果我正在执行像STL或boost这样的头文件实现,该怎么办? (6认同)
  • @Billy ONeal:不.这是一个好的设计而不是意见的问题.将其放在命名空间中不会保护它不被使用.它现在是公共API的一部分,需要永久维护. (5认同)
  • @Billy ONeal:我喜欢这个约定并将开始使用它。但我还是第一次听说。这是他们网站上记录的增强约定吗? (2认同)

Kos*_*Kos 138

嵌套类就像常规类一样,但是:

  • 他们有额外的访问限制(因为类定义中的所有定义都有),
  • 它们不会污染给定的命名空间,例如全局命名空间.如果你觉得B类与A类有如此深的联系,但是A和B的对象不一定相关,那么你可能希望只能通过对A类进行范围化来访问B类(它将被称为A ::类).

一些例子:

公开嵌套类将其置于相关类的范围内


假设您想要一个SomeSpecificCollection可以聚合类对象的类Element.然后你可以:

  1. 声明两个类:SomeSpecificCollectionElement- 坏,因为名称"元素"足够通用,以便导致可能的名称冲突

  2. 引入命名空间someSpecificCollection并声明类someSpecificCollection::CollectionsomeSpecificCollection::Element.没有名字冲突的风险,但是它会变得更加冗长吗?

  3. 声明两个全局类SomeSpecificCollectionSomeSpecificCollectionElement-其中有轻微的缺陷,但可能确定.

  4. 将全局类SomeSpecificCollection和类声明Element为其嵌套类.然后:

    • 您不会冒任何名称冲突的风险,因为Element不在全局命名空间中,
    • SomeSpecificCollection你的实现中,你只需要Element和其他地方一样SomeSpecificCollection::Element- 看起来+ - 与3.相同,但更清楚
    • 它很简单,它是"特定集合的元素",而不是"集合的特定元素"
    • 它是可见的,SomeSpecificCollection也是一个类.

在我看来,最后一个版本绝对是最直观的,因此也是最好的设计.

让我强调一下 - 这与制作两个名字更详细的全局类没有太大区别.它只是一个小小的细节,但它使代码更清晰.

在类范围内引入另一个范围


这对于引入typedef或枚举特别有用.我将在这里发布一个代码示例:

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
};
Run Code Online (Sandbox Code Playgroud)

然后会打电话给:

Product p(Product::FANCY, Product::BOX);
Run Code Online (Sandbox Code Playgroud)

但是当查看代码完成提议时Product::,通常会列出所有可能的枚举值(BOX,FANCY,CRATE)并且很容易在这里犯一个错误(C++ 0x的强类型枚举有点解决这个问题,但没关系).

但是如果你使用嵌套类为这些枚举引入额外的范围,事情可能如下所示:

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
};
Run Code Online (Sandbox Code Playgroud)

然后电话看起来像:

Product p(Product::ProductType::FANCY, Product::ProductBoxType::BOX);
Run Code Online (Sandbox Code Playgroud)

然后通过键入Product::ProductType::IDE,只能获得建议的所需范围的枚举.这也降低了犯错的风险.

当然,对于小类来说可能不需要这样,但是如果有很多枚举,那么它会让客户端程序员更容易.

同样,如果您有需要,可以在模板中"组织"一大堆typedef.有时它是一种有用的模式.

PIMPL习语


PIMPL(指向IMPLementation的指针的缩写)是一个习惯用法,用于从头中删除类的实现细节.每当标题的"实现"部分发生变化时,这就减少了重新编译类的需要,具体取决于类的标题.

它通常使用嵌套类实现:

XH:

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

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
Run Code Online (Sandbox Code Playgroud)

如果完整的类定义需要来自某个外部库的类型定义,而这个外部库具有繁重或丑陋的头文件(使用WinAPI),这将特别有用.如果使用PIMPL,则只能包含任何特定于WinAPI的功能,.cpp并且永远不会包含它.h.

  • `struct Impl; std :: auto_ptr <Impl> impl;`这个错误由​​Herb Sutter推广.不要在不完整的类型上使用auto_ptr,或者至少采取预防措施以避免生成错误的代码. (3认同)
  • @Billy奥尼尔:据我所知,你可以声明`auto_ptr`不完全类型的大多数实现,但在技术上它是UB不像一些C++ 0x中的模板(如:`unique_ptr`),其中已作出了明确模板参数可能是不完整的类型,并且类型必须完整.(例如使用`~unique_ptr`) (2认同)
  • @Billy ONeal:在C++ 03中17.4.6.3 [lib.res.on.functions]说"特别是在以下情况下效果未定义:[...]如果使用不完整的类型作为模板参数在实例化模板组件时." 而在C++ 0x中,它表示"如果在实例化模板组件时将不完整的类型用作模板参数,则除非特别允许该组件." 然后(例如):"unique_ptr"的模板参数`T`可能是不完整的类型. (2认同)
  • @MilesRout 这太笼统了。取决于客户端代码是否允许继承。规则:如果您确定不会通过基类指针进行删除,则虚拟 dtor 是完全多余的。 (2认同)
  • @IsaacPascual aww,我应该更新,因为我们有'enum class`. (2认同)

Joh*_*ing 21

我不太多使用嵌套类,但我偶尔会使用它们.特别是当我定义某种数据类型时,我想要定义一个为该数据类型设计的STL仿函数.

例如,考虑Field具有ID号,类型代码和字段名称的泛型类.如果我想通过ID号或名称搜索vector这些Fields,我可能会构造一个仿函数来:

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_;
  };
};
Run Code Online (Sandbox Code Playgroud)

然后需要搜索这些Fields的代码可以matchField类本身中使用作用域:

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


Yeo*_*Yeo 13

可以使用嵌套类实现Builder模式.特别是在C++中,我个人认为它在语义上更清晰.例如:

class Product{
    public:
        class Builder;
}
class Product::Builder {
    // Builder Implementation
}
Run Code Online (Sandbox Code Playgroud)

而不是:

class Product {}
class ProductBuilder {}
Run Code Online (Sandbox Code Playgroud)