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

Why would one use nested classes in C++?

提问人:bespectacled 提问时间:1/1/2011 最后编辑:Michael Mabespectacled 更新时间:7/18/2023 访问量:220242

问:

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

但我仍然难以理解他们的目的。有人可以帮我吗?

C++ 嵌套 的内部类

评论

22赞 Billy ONeal 1/1/2011
我对 C++ 中的嵌套类的建议是不要使用嵌套类。
12赞 user229044 1/1/2011
他们和普通班级一模一样......除了嵌套。当一个类的内部实现非常复杂,以至于它最容易由几个较小的类建模时,请使用它们。
17赞 John Dibling 1/1/2011
@Billy:为什么?对我来说似乎过于宽泛。
40赞 John Dibling 1/1/2011
我仍然没有看到为什么嵌套类本质上是不好的论点。
10赞 Billy ONeal 1/1/2011
@7vies:1。因为这根本不是必需的——你可以对外部定义的类做同样的事情,这缩小了任何给定变量的范围,这是一件好事。2. 因为你可以做嵌套类可以做的所有事情。3. 因为它们在避免长线已经很困难的环境中增加了额外的缩进级别 4.因为您在单个声明中声明了两个概念上独立的对象,等等。typedefclass

答:

26赞 John Dibling 1/1/2011 #1

我不怎么使用嵌套类,但我确实时不时地使用它们。特别是当我定义某种数据类型时,然后我想定义一个为该数据类型设计的 STL 函子。

例如,考虑一个具有 ID 号、类型代码和字段名称的泛型类。如果我想通过 ID 号或名称搜索其中的一个,我可能会构造一个函子来这样做:FieldvectorField

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

然后,需要搜索这些 s 的代码可以使用类本身中的 scoped:FieldmatchField

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

评论

0赞 bespectacled 1/1/2011
感谢您提供出色的示例和评论,尽管我不太了解 STL 函数。我注意到match()中的构造函数是公共的。我假设构造函数不必总是公开的,在这种情况下,它不能在类外部实例化。
1赞 John Dibling 1/1/2011
@user:对于 STL 函子,构造函数确实需要是公共的。
1赞 John Dibling 1/1/2011
@Billy:我仍然没有看到任何具体的理由来解释为什么嵌套类是坏的。
0赞 Billy ONeal 1/1/2011
@John:所有的编码风格指南都归结为一个意见问题。我在这里的几条评论中列出了几个原因,所有这些原因(在我看来)都是合理的。只要代码有效并且没有调用未定义的行为,就不能提出“事实”参数。但是,我认为您在此处提供的代码示例指出了我避免嵌套类的一个重要原因 - 即名称冲突。
1赞 Miles Rout 3/16/2013
当然,从技术上讲,更喜欢内联而不是宏!
284赞 Martin York 1/1/2011 #2

嵌套类对于隐藏实现细节来说很酷。

列表:

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 的一部分,必须永远维护。通过将类设为私有,我不仅隐藏了实现,我还说这是我的,我可以随时更改它,这样你就不能使用它了。

看看或者它们都包含隐藏的类(或者它们?关键是它们可能会也可能不会,但是由于实现是私有的并且是隐藏的,因此 STL 的构建者能够在不影响您使用代码的方式的情况下更新代码,或者在 STL 周围留下许多旧包袱,因为他们需要与一些傻瓜保持向后兼容性,这些傻瓜决定使用隐藏在其中的 Node 类。std::liststd::maplist

评论

12赞 Billy ONeal 1/1/2011
如果您这样做,则根本不应该在头文件中公开。Node
11赞 Martin York 1/1/2011
@Billy ONeal:如果我正在做一个头文件实现,比如 STL 或 boost,该怎么办?
9赞 Martin York 1/1/2011
@Billy ONeal:没有。这是一个好的设计问题,而不是意见的问题。将其放在命名空间中并不能保护它不被使用。它现在是公共 API 的一部分,需要永久维护。
30赞 Martin York 1/1/2011
@Billy ONeal:它可以防止意外使用。它还记录了它是私有的,不应该使用的事实(除非你做一些愚蠢的事情,否则不能使用)。因此,您不需要支持它。将它放在命名空间中会使它成为公共 API 的一部分(您在此对话中一直缺少的东西。公共 API 意味着您需要支持它)。
12赞 SasQ 3/31/2014
@Billy ONeal:嵌套类与嵌套命名空间相比具有一些优势:您不能创建命名空间的实例,但可以创建类的实例。至于约定:与其依赖这些约定,不如记住自己,最好依靠为您跟踪它们的编译器。detail
160赞 Kos 1/1/2011 #3

嵌套类与常规类一样,但是:

  • 它们具有额外的访问限制(就像类定义中的所有定义一样),
  • 它们不会污染给定的命名空间,例如全局命名空间。如果您觉得类 B 与类 A 有很深的联系,但 A 和 B 的对象不一定相关,那么您可能希望类 B 只能通过作用域 A 类来访问(它被称为 A::Class)。

一些例子:

公开嵌套类,将其放在相关类的作用域中


假设你想要一个类,它将聚合类的对象。然后,您可以:SomeSpecificCollectionElement

  1. 声明两个类:和 - bad,因为名称“Element”足够通用,可能会导致可能的名称冲突SomeSpecificCollectionElement

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

  3. 声明两个全局类和 - 这有小缺点,但可能没问题。SomeSpecificCollectionSomeSpecificCollectionElement

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

    • 您不会冒任何名称冲突的风险,因为 Element 不在全局命名空间中,
    • 在实现中,你只引用 ,其他任何地方都称为 - 看起来 +- 与 3. 相同,但更清晰SomeSpecificCollectionElementSomeSpecificCollection::Element
    • 它变得很简单,它是“特定集合的元素”,而不是“集合的特定元素”
    • 可见,这也是一个类。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);

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

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

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::

当然,对于小类来说,这可能不需要,但如果一个有很多枚举,那么它使客户端程序员的工作变得更容易。

同样,如果你需要的话,你可以在模板中“组织”一大堆 typedef。有时这是一种有用的模式。

PIMPL成语


PIMPL(指向 IMPLementation 的指针的缩写)是一个惯用语,可用于从标头中删除类的实现细节。这样就减少了每当标头的“实现”部分发生更改时,根据类标头重新编译类的需要。

它通常使用嵌套类实现:

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,则只能将任何特定于 WinAPI 的功能包含在 中,而绝不能将其包含在 中。.cpp.h

评论

3赞 Gene Bushuyev 1/1/2011
struct Impl; std::auto_ptr<Impl> impl;这个错误是由赫伯·萨特(Herb Sutter)推广的。不要对不完整的类型使用auto_ptr,或者至少采取预防措施以避免生成错误的代码。
2赞 CB Bailey 1/1/2011
@Billy ONeal:据我所知,您可以在大多数实现中声明不完整的类型,但从技术上讲,它是 UB,不像 C++0x 中的某些模板(例如),其中已明确模板参数可能是不完整的类型,并且该类型必须是完整的。(例如,使用auto_ptrunique_ptr~unique_ptr)
2赞 CB Bailey 1/1/2011
@Billy ONeal:在 C++03 中,17.4.6.3 [lib.res.on.functions] 说:“特别是,在以下情况下,效果是未定义的:[...]如果在实例化模板组件时将不完整类型用作模板参数“,而在 C++0x 中,它说”如果在实例化模板组件时将不完整类型用作模板参数,除非该组件特别允许“,然后(例如):”的模板参数可能是不完整的类型。Tunique_ptr
2赞 Kos 3/16/2013
@MilesRout 这太笼统了。取决于是否允许继承客户端代码。规则:如果您确定不会通过基类指针删除,则虚拟 dtor 是完全多余的。
2赞 Kos 4/11/2017
@IsaacPascual,哇,我现在应该更新它,因为我们有.enum class
16赞 Yeo 9/28/2015 #4

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

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

而不是:

class Product {}
class ProductBuilder {}

评论

0赞 irsis 6/26/2018
当然,如果只有一个构建,它会起作用,但如果需要多个混凝土构建器,它会变得令人讨厌。人们应该谨慎地做出设计决策:)
1赞 Mikhail Veselov 5/17/2021 #5

我认为使类嵌套而不仅仅是友元类的主要目的是能够在派生类中继承嵌套类。友谊在C++中不是继承的。

-3赞 maxim 7/10/2021 #6

您还可以考虑 main 函数的 first class ass 类型,您可以在其中启动所有需要的类来工作。例如,类游戏,启动所有其他类,如窗口、英雄、敌人、关卡等。这样,您就可以从主功能中摆脱所有这些东西。您可以在其中创建游戏的 obiect,并可能做一些与 Gemente 无关的额外外部调用。