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


188

有人可以将我引向一些不错的资源来理解和使用嵌套类吗?我有一些像《编程原理》之类的材料,以及诸如《IBM Knowledge Center-Nested Classes》之类的东西。

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


15
我对C ++中嵌套类的建议是不要使用嵌套类。
Billy ONeal,2010年

7
它们与常规类完全一样...除了嵌套。当类的内部实现非常复杂以至于可以由几个较小的类轻松建模时,可以使用它们。
meagar

12
@比利:为什么?对我来说似乎太宽泛了。
约翰·迪布林

30
我仍然没有看到为什么嵌套类本质上不好的争论。
John Dibling 2010年

7
@ 7vies:1.因为根本没有必要-您可以对外部定义的类进行相同的操作,这可以减小任何给定变量的范围,这是一件好事。2.因为您可以做嵌套类可以做的所有事情typedef。3.因为他们增加缩进一个附加级别的环境下,避免排长队已经很难4.因为你是在一个声明两个概念上不同的对象class申报等
比利·奥尼尔

Answers:


228

嵌套类非常适合隐藏实现细节。

清单:

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::list或者std::map它们都包含隐藏的类(或者它们是吗?)。关键是它们可能存在或不存在,但是因为实现是私有的并且是隐藏的,所以STL的构建者能够在不影响您使用代码的方式的情况下更新代码,或者因为需要它们而在STL周围留下了很多旧包。为了与那些决定使用隐藏在内部的Node类的傻瓜保持向后兼容list


9
如果您正在执行此操作,则Node根本不应该在头文件中公开它。
Billy ONeal 2010年

6
@Billy ONeal:如果我正在执行STL或boost这样的头文件实现该怎么办。
马丁·约克

5
@Billy ONeal:不。这是一个好的设计问题,而不是意见。将其放在命名空间中并不能防止其使用。现在,它是公共API的一部分,需要永久保留。
马丁·约克

21
@Billy ONeal:它可以防止意外使用。它还记录了以下事实:它是私有的,不应使用(除非您做一些愚蠢的操作,否则不能使用)。因此,您不需要支持它。将其放在命名空间中使其成为公共API的一部分(在此对话中您会一直漏掉一些东西。公共API意味着您需要支持它)。
马丁·约克

10
@Billy ONeal:嵌套类比嵌套名称空间具有一些优势:您不能创建名称空间的实例,但是可以创建类的实例。关于detail约定:与其依赖于这样的约定,不如记住自己,最好依靠编译器为您跟踪它们。
2014年

141

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

  • 它们具有附加的访问限制(就像类定义中的所有定义一样),
  • 它们不会污染给定的名称空间,例如全局名称空间。如果您认为B类与A类之间的联系如此紧密,但是A和B的对象不一定相关联,那么您可能希望仅通过对A类进行范围界定来访问B类(将其称为A ::类)。

一些例子:

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


假设您想拥有一个SomeSpecificCollection将聚集class对象的类Element。然后,您可以:

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

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

  3. 声明两个全局类SomeSpecificCollectionSomeSpecificCollectionElement-有一些小缺点,但可能还可以。

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

    • 您不会冒任何名称冲突的风险,因为Element不在全局名称空间中,
    • 在实现中,SomeSpecificCollection您将just 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);

然后,通过输入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;
}

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 2010年

2
@Billy ONeal:据我所知,您可以auto_ptr在大多数实现中声明一个不完整的类型,但是从技术上讲,它是UB,与C ++ 0x中的某些模板(例如unique_ptr)不同,在该模板中已明确指出template参数可能是一个不完整的类型,以及该类型必须完整的位置。(例如使用~unique_ptr
CB Bailey

2
@Billy ONeal:在C ++ 03 17.4.6.3中[lib.res.on.functions]说“特别是在以下情况下,效果未定义:[...]如果使用不完整的类型作为模板参数实例化模板组件时。” 而在C ++ 0x中,它表示“除非实例化模板组件,否则在实例化模板组件时是否将不完整的类型用作模板参数”。和以后(例如):“模板参数Tunique_ptr可能是一个不完整的类型。”
CB Bailey

1
@MilesRout太笼统了。取决于是否允许客户端代码继承。规则:如果确定不会通过基类指针删除,则虚拟dtor是完全多余的。
科斯

2
@IsaacPascual aww,我现在应该更新它enum class
科斯

21

我没有太多使用嵌套类,但是我时不时地使用它们。尤其是当我定义某种数据类型时,然后我想定义为该数据类型设计的STL函子。

例如,考虑Field具有ID号,类型代码和字段名称的通用类。如果我想搜索vector这些中Field的任一ID号码或姓名S,我可能会构建一个仿函数可以这样做:

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()中的构造函数是公共的。我认为构造函数不必总是公开的,在这种情况下,不能在类外部实例化它。
带眼镜

1
@user:对于STL函子,构造函数的确需要是公共的。
John Dibling 2010年

1
@Billy:我仍然没有看到为什么嵌套类不好的任何具体理由。
约翰·迪布林

@John:所有编码风格指南都取决于观点。我在这里的几条评论中列出了几个原因,所有这些(我认为)都是合理的。只要代码有效且没有调用未定义的行为,就没有“事实”参数可以使用。但是,我认为您在此处放置的代码示例指出了我避免嵌套类的一个重要原因-即名称冲突。
Billy ONeal 2010年

1
当然,出于技术原因,我更倾向于使用内联而不是宏!
Miles Rout

13

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

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

而不是:

class Product {}
class ProductBuilder {}

当然,如果只有一个构建,它将起作用,但是如果需要多个具体构建器,它将变得令人讨厌。人们应该谨慎地做出设计决策:)
irsis
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.