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

237

请有人向我指出一些理解和使用嵌套类的好资源?我已经有一些材料,比如IBM知识中心 - 嵌套类之类的内容。

但我仍然很难理解它们的目的。请有人帮助我吗?


22
我对在C++中使用嵌套类的建议就是不要使用嵌套类。我建议避免使用嵌套类来使代码更易于理解和维护。 - Billy ONeal
12
它们就像普通类一样,只不过是嵌套的。当一个类的内部实现非常复杂,最容易用几个较小的类来建模时,请使用它们。 - user229044
17
@Billy:为什么?对我来说似乎过于笼统。 - John Dibling
40
到目前为止,我还没有看到一个说明为什么嵌套类本质上不好的论点。 - John Dibling
10
  1. 之所以不需要嵌套类是因为可以使用外部定义的类来减小给定变量的作用范围,这是一件好事情。
  2. 可以通过 typedef 实现与嵌套类相同的功能。
  3. 在一个本来就很难避免长行的环境中,嵌套类会增加额外的缩进级别。
  4. 因为你在单个 class 声明中声明了两个概念上独立的对象等。
- Billy ONeal
显示剩余16条评论
6个回答

284

嵌套类很棒,可以隐藏实现细节。

列表:

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

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

看看 std::liststd::map,它们都包含隐藏的类(或者它们是否包含呢?)。重点是它们可能会或可能不会,但是因为实现是私有和隐藏的,STL 的构建者能够更新代码而不影响您使用代码,或留下许多旧代码在 STL 中,因为他们需要与决定使用 list 中隐藏的 Node 类的某些傻瓜保持向后兼容性。


12
如果你这样做,那么在头文件中就不应该暴露 Node - Billy ONeal
11
如果我正在进行类似STL或boost的头文件实现,该怎么办? - Martin York
9
@Billy ONeal:不是基于个人观点而言,而是关于良好设计的问题。将其放入命名空间并不能保护它免受使用。它现在是公共API的一部分,需要永久维护。 - Martin York
30
@Billy ONeal:它可以防止意外使用。它还记录了它是私有的并且不应该被使用(除非你做了一些愚蠢的事情)。因此,你不需要支持它。将它放在命名空间中使它成为公共 API 的一部分(这是你在这次对话中一直忽略的。公共 API 意味着你需要支持它)。 - Martin York
12
嵌套类相比嵌套命名空间具有优势:你不能创建命名空间的实例,但可以创建类的实例。关于“detail”约定:不要依赖这种约定而是要依赖编译器来为您跟踪它们。 - SasQ
显示剩余11条评论

160
嵌套类与普通类一样,但有以下不同:
  • 它们具有额外的访问限制(因为类定义中的所有定义都是如此)。
  • 它们不会污染给定的命名空间,例如全局命名空间。如果您认为类B与类A紧密相关,但A和B的对象并不一定相关,则可能希望仅通过作用域限定A类来访问类B(它将被称为A :: Class)。
一些例子:

公开嵌套类以将其放在相关类的范围内


假设您想要一个类 SomeSpecificCollection ,该类将聚合类 Element 的对象。然后,您可以:
  1. 声明两个类: SomeSpecificCollection Element - 不好,因为名称“ Element”足够通用,可能会导致可能的名称冲突
  2. 引入命名空间 someSpecificCollection 并声明类 someSpecificCollection :: Collection someSpecificCollection :: Element 。没有名称冲突的风险,但是否可以更冗长?
  3. 声明两个全局类 SomeSpecificCollection SomeSpecificCollectionElement - 具有轻微的缺点,但可能是可以接受的。
  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
};

那么,我们可以称之为:

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

但是,当查看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
};

然后,调用看起来像这样:

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

然后在IDE中键入Product::ProductType::,将只建议所需范围内的枚举。这也减少了犯错误的风险。
当然,对于小型类可能不需要,但如果有很多枚举,则可以为客户端程序员简化事情。
同样地,如果有需要,您可以在模板中“组织”大量的typedefs。这是一种有用的模式。
PIMPL(指向实现的指针)是一种有用的习惯用法,可用于从头文件中删除类的实现细节。这减少了在头文件更改“实现”部分时重新编译依赖于该类头文件的类的需要。
通常使用嵌套类来实现:
X.h:
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,则可以仅在.cpp中封装任何WinAPI特定功能,并永远不要在.h中包含它。


3
结构体 Impl; std::auto_ptr<Impl> impl;这个错误由Herb Sutter广为流传。不要在不完整的类型上使用auto_ptr,或者至少采取预防措施以避免生成错误代码。 - Gene Bushuyev
2
据我所知,在大多数实现中,您可以声明不完整类型的 auto_ptr,但从技术上讲,这是未定义行为。与 C++0x 中一些模板(例如 unique_ptr)不同的是,已明确指出模板参数可以是不完整类型,并且必须完整确定该类型的位置(例如使用 ~unique_ptr)。@Billy ONeal - CB Bailey
2
@Billy ONeal:在C++03 17.4.6.3 [lib.res.on.functions]中说:“特别地,在以下情况下,效果是未定义的:[...]如果在实例化模板组件时使用不完整类型作为模板参数。”而在C++0x中则说:“如果在实例化模板组件时使用不完整类型作为模板参数,除非该组件明确允许。”并且后面(例如):“unique_ptr的模板参数T可以是不完整类型。” - CB Bailey
2
@MilesRout 这太笼统了。这取决于客户端代码是否允许继承。规则:如果您确定不会通过基类指针删除,则虚拟析构函数完全是多余的。 - Kos
2
@IsaacPascual 哎呀,我们现在有enum class了,我应该更新一下。 - Kos
显示剩余12条评论

26

我不常使用嵌套类,但有时会用到它们。特别是当我定义某种数据类型,然后想要定义一个适用于该数据类型的STL函数对象时。

例如,考虑一个通用的Field类,它具有ID号码、类型代码和字段名称。如果我想按照ID号或名称搜索这些Field对象组成的vector容器,我可能会构造一个函数对象来实现:

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 的代码可以使用在 Field 类中定义的 match

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

1
在STL函数对象的情况下,构造函数确实需要是公共的。 - John Dibling
1
@Billy:我仍然没有看到任何具体的理由说明为什么嵌套类是不好的。 - John Dibling
@Billy:如果你认为嵌套类不好,但是没有技术依据支持你的观点,那就是你的权利。我可能会认为,如果没有技术理由,你的观点更像是一时兴起的想法,但你有权这么认为。但是你应该将你的观点单独发布为答案,而不是在评论其他人的答案时发表,这样如果有人认为你错了,他们可以与你讨论。 - John Dibling
1
当然,有技术上的原因更喜欢使用内联函数而不是宏! - Miles Rout
@Billy ONeal:不仅限于 friend。还包括同一类的成员函数,例如静态工厂方法。 - SasQ
显示剩余5条评论

16

可以使用嵌套类实现建造者模式。特别是在C++中,我个人认为它的语义更加清晰。例如:

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

不是:

class Product {}
class ProductBuilder {}

当只有一个构建时,它肯定会起作用,但如果需要多个具体的构建器,则会变得非常棘手。因此,我们应该谨慎地做出设计决策 :) - irsis

1
我认为将一个类嵌套而不仅仅是作为友元类的主要目的是能够在派生类中继承嵌套类。在C++中,友谊是不能被继承的。

-3
你也可以考虑使用一种主函数的一流类型,其中你初始化所有需要协同工作的类。例如,类Game会初始化所有其他类,如窗口、英雄、敌人、关卡等等。这样,你就可以从主函数本身中摆脱所有这些东西。在那里,你可以创建Game对象,并可能进行一些与Gemente本身无关的额外外部调用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接