Por que alguém usaria classes aninhadas em C ++?

188

Alguém pode me indicar alguns recursos interessantes para entender e usar classes aninhadas? Eu tenho alguns materiais como Princípios de Programação e coisas como este IBM Knowledge Center - Nested Classes

Mas ainda estou tendo problemas para entender o propósito deles. Alguém poderia me ajudar, por favor?

com óculos
fonte
15
Meu conselho para classes aninhadas em C ++ é simplesmente não usar classes aninhadas.
Billy ONeal
7
Eles são exatamente como aulas regulares ... exceto aninhados. Use-os quando a implementação interna de uma classe for tão complexa que ela possa ser modelada com mais facilidade por várias classes menores.
meagar
12
@ Billy: Por quê? Parece excessivamente amplo para mim.
John Dibling
30
Ainda não vi um argumento sobre por que classes aninhadas são ruins por natureza.
John Dibling
7
@ 7vies: 1. simplesmente porque não é necessário - você pode fazer o mesmo com classes definidas externamente, o que reduz o escopo de qualquer variável, o que é uma coisa boa. 2. porque você pode fazer tudo o que as classes aninhadas podem fazer typedef. 3. porque adicionam um nível adicional de recuo em um ambiente onde evitando longas filas já é difícil 4. porque você está declarando dois objetos conceitualmente separadas em uma única classdeclaração, etc.
Billy ONeal

Respostas:

228

Classes aninhadas são legais para ocultar detalhes de implementação.

Lista:

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

Aqui, não quero expor o Node, pois outras pessoas podem decidir usar a classe e isso me impediria de atualizar minha classe, pois qualquer coisa exposta faz parte da API pública e deve ser mantida para sempre . Ao tornar a classe privada, não apenas oculto a implementação, mas também digo que é minha e posso alterá-la a qualquer momento para que você não possa usá-la.

Veja std::listou std::maptodos eles contêm classes ocultas (ou não?). A questão é que eles podem ou não, mas como a implementação é privada e oculta, os construtores da STL conseguiram atualizar o código sem afetar a maneira como você o utilizava, ou deixando muitas bagagens antigas em volta da STL porque precisam para manter a compatibilidade com versões anteriores de algum tolo que decidiu que queria usar a classe Node que estava oculta list.

Martin York
fonte
9
Se você estiver fazendo isso, Nodenão deverá ser exposto no arquivo de cabeçalho.
Billy ONeal
6
@ Billy ONeal: E se eu estiver fazendo uma implementação de arquivo de cabeçalho como o STL ou boost.
Martin York
5
@ Billy ONeal: Não. É uma questão de bom design, não de opinião. Colocá-lo em um espaço para nome não o protege de uso. Agora faz parte da API pública que precisa ser mantida para perpetuidade.
Martin York
21
@ Billy ONeal: Protege contra uso acidental. Ele também documenta o fato de ser privado e não deve ser usado (não pode ser usado a menos que você faça algo estúpido). Portanto, você não precisa apoiá-lo. Colocá-lo em um espaço para nome faz parte da API pública (algo que você continua perdendo nessa conversa. API pública significa que você precisa apoiá-lo).
Martin York
10
@ Billy ONeal: A classe aninhada tem alguma vantagem sobre o espaço para nome aninhado: Você não pode criar instâncias de um espaço para nome, mas pode criar instâncias de uma classe. Quanto à detailconvenção: em vez disso, dependendo de tais convenções, é preciso ter em mente a si mesmo, é melhor depender do compilador que as controla para você.
SasQ 31/03
142

Classes aninhadas são como classes regulares, mas:

  • eles têm restrição de acesso adicional (como todas as definições dentro de uma definição de classe),
  • eles não poluem o espaço para nome fornecido , por exemplo, espaço para nome global. Se você acha que a classe B está tão profundamente conectada à classe A, mas os objetos de A e B não estão necessariamente relacionados, convém que a classe B seja acessível apenas através do escopo da classe A (ela seria chamada de A ::Classe).

Alguns exemplos:

Aninhando publicamente classe para colocá-la em um escopo de classe relevante


Suponha que você queira ter uma classe SomeSpecificCollectionque agregue objetos da classe Element. Você pode:

  1. declare duas classes: SomeSpecificCollectione Element- ruim, porque o nome "Elemento" é geral o suficiente para causar um possível conflito de nome

  2. introduza um espaço para nome someSpecificCollectione declare classes someSpecificCollection::Collectione someSpecificCollection::Element. Não há risco de conflito de nomes, mas pode ficar mais detalhado?

  3. declarar duas classes globais SomeSpecificCollectione SomeSpecificCollectionElement- que tem desvantagens menores, mas provavelmente está OK.

  4. declarar classe global SomeSpecificCollectione classe Elementcomo sua classe aninhada. Então:

    • você não corre o risco de conflitos de nome, pois o Element não está no espaço de nomes global,
    • na implementação de SomeSpecificCollectionvocê se refere apenas Element, e em todos os outros lugares, como SomeSpecificCollection::Element- que parece + - o mesmo que 3., mas mais claro
    • fica bem simples que seja "um elemento de uma coleção específica", não "um elemento específico de uma coleção"
    • é visível que SomeSpecificCollectiontambém é uma classe.

Na minha opinião, a última variante é definitivamente o design mais intuitivo e, portanto, o melhor.

Deixe-me enfatizar: não é uma grande diferença criar duas classes globais com nomes mais detalhados. É apenas um pequeno detalhe, mas imho torna o código mais claro.

Introduzindo outro escopo dentro de um escopo de classe


Isso é especialmente útil para a introdução de typedefs ou enumerações. Vou apenas postar um exemplo de código aqui:

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

Um então chamará:

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

Mas, ao analisar as propostas de conclusão de código para Product::, geralmente é possível obter todos os valores possíveis de enumeração (BOX, FANCY, CRATE) e é fácil cometer um erro aqui (as enumerações fortemente tipadas do C ++ 0x resolvem isso, mas não importa) )

Mas se você introduzir um escopo adicional para essas enumerações usando classes aninhadas, as coisas podem se parecer com:

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

Em seguida, a chamada se parece com:

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

Ao digitar Product::ProductType::um IDE, obteremos apenas as enumerações do escopo desejado sugerido. Isso também reduz o risco de cometer um erro.

É claro que isso pode não ser necessário para turmas pequenas, mas se houver muitas enumerações, isso facilitará as coisas para os programadores clientes.

Da mesma forma, você pode "organizar" um grande número de typedefs em um modelo, se você precisar. Às vezes é um padrão útil.

O idioma do PIMPL


O PIMPL (abreviação de Pointer to IMPLementation) é um idioma útil para remover os detalhes de implementação de uma classe do cabeçalho. Isso reduz a necessidade de recompilar classes, dependendo do cabeçalho da classe sempre que a parte "implementação" do cabeçalho é alterada.

Geralmente é implementado usando uma classe aninhada:

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

Isso é particularmente útil se a definição de classe completa precisar da definição de tipos de alguma biblioteca externa que tenha um arquivo de cabeçalho pesado ou apenas feio (use o WinAPI). Se você usar o PIMPL, poderá incluir qualquer funcionalidade específica do WinAPI apenas .cppe nunca incluí-la .h.

Kos
fonte
3
struct Impl; std::auto_ptr<Impl> impl; Este erro foi popularizado por Herb Sutter. Não use auto_ptr em tipos incompletos, ou pelo menos tome precauções para evitar que código errado seja gerado.
Gene Bushuyev
2
@ Billy ONeal: Tanto quanto sei, você pode declarar um auto_ptrtipo incompleto na maioria das implementações, mas tecnicamente é UB diferente de alguns dos modelos em C ++ 0x (por exemplo unique_ptr), onde foi explicitado que o parâmetro do modelo pode ser um tipo incompleto e onde exatamente o tipo deve estar completo. (por exemplo, uso de ~unique_ptr)
CB Bailey
2
@ Billy ONeal: em C ++ 03 17.4.6.3 [lib.res.on.functions] diz "Em particular, os efeitos são indefinidos nos seguintes casos: [...] se um tipo incompleto é usado como argumento de modelo ao instanciar um componente de modelo ". enquanto em C ++ 0x diz "se um tipo incompleto for usado como argumento de modelo ao instanciar um componente de modelo, a menos que seja especificamente permitido para esse componente". e mais tarde (por exemplo): "O parâmetro Tdo modelo de unique_ptrpode ser um tipo incompleto."
CB Bailey
1
@MilesRout Isso é geral demais. Depende se o código do cliente pode herdar. Regra: Se você tiver certeza de que não será excluído por meio de um ponteiro de classe base, o dtor virtual será completamente redundante.
Kos
2
@IsaacPascual aww, devo atualizar isso agora que temos enum class.
Kos
21

Não uso muito classes aninhadas, mas uso-as de vez em quando. Especialmente quando eu defino algum tipo de tipo de dados e, em seguida, desejo definir um functor STL projetado para esse tipo de dados.

Por exemplo, considere uma Fieldclasse genérica que tenha um número de identificação, um código de tipo e um nome de campo. Se eu quiser procurar um vectordesses Fields por número de identificação ou nome, posso construir um functor para fazer isso:

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

Em seguida, o código que precisa procurar esses Fields pode usar o matchescopo dentro da Fieldprópria classe:

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));
John Dibling
fonte
Obrigado pelo ótimo exemplo e comentários, embora eu não conheça bem as funções do STL. Percebo que os construtores em match () são públicos. Suponho que os construtores nem sempre precisam ser públicos, caso em que não podem ser instanciados fora da classe.
bespectacled
1
@user: No caso de um functor STL, o construtor precisa ser público.
John Dibling
1
@ Billy: Ainda não vi nenhum raciocínio concreto sobre por que as classes aninhadas são ruins.
John Dibling
@ John: Todas as diretrizes de estilo de codificação se resumem a uma questão de opinião. Listei várias razões em vários comentários por aqui, todos os quais (na minha opinião) são razoáveis. Não há argumento "factual" que possa ser feito desde que o código seja válido e invoque nenhum comportamento indefinido. No entanto, acho que o exemplo de código que você coloca aqui aponta uma grande razão pela qual evito classes aninhadas - ou seja, o nome entra em conflito.
precisa
1
É claro que existem razões técnicas para preferir linhas inline às macros !!
Route de milhas
14

Pode-se implementar um padrão Builder com classe aninhada . Especialmente em C ++, pessoalmente acho semanticamente mais limpo. Por exemplo:

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

Ao invés de:

class Product {}
class ProductBuilder {}
Yeo
fonte
Claro, funcionará se houver apenas uma construção, mas ficará desagradável se houver a necessidade de ter vários construtores de concreto. Um deve ser cuidadosamente tomar decisões de design :)
irsis