Pergunta

Eu estou procurando a definição de quando eu estou autorizado a fazer declaração para a frente de uma classe no arquivo de cabeçalho de outra classe:

Am I autorizados a fazê-lo para uma classe base, para uma classe realizada como um membro, para uma classe passado para função de membro por referência, etc.?

Foi útil?

Solução

Coloque-se na posição do compilador: quando você declara a frente um tipo, todo o compilador sabe é que este tipo existe; ele não sabe nada sobre seu tamanho, membros ou métodos. É por isso que ele é chamado de tipo incompleto . Portanto, você não pode usar o tipo de declarar um membro, ou de uma classe base, uma vez que o compilador precisa saber o layout do tipo.

Assumindo a seguinte declaração para a frente.

class X;

Aqui está o que você pode e não pode fazer.

O que você pode fazer com um tipo incompleto:

  • declarar um membro a ser um ponteiro ou uma referência para o tipo incompleto:

    class Foo {
        X *p;
        X &r;
    };
    
  • Declare funções ou métodos que aceita / retornar tipos incompletos:

    void f1(X);
    X    f2();
    
  • Definir funções ou métodos que aceita / ponteiros de retorno / referências com o modelo incompleto (mas sem o uso de seus membros):

    void f3(X*, X&) {}
    X&   f4()       {}
    X*   f5()       {}
    

O que você não pode fazer com um tipo incompleto:

  • Use-o como uma classe base

    class Foo : X {} // compiler error!
    
  • Use-o para declarar um membro:

    class Foo {
        X m; // compiler error!
    };
    
  • Definir funções ou métodos que utilizam este tipo

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • Use seus métodos ou campos, na verdade tentando dereference uma variável com tipo incompleto

    class Foo {
        X *m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
    

Quando se trata de modelos, não existe uma regra absoluta:. Se você pode usar um tipo incompleto como um parâmetro do modelo é dependente da forma como o tipo é usado no modelo

Por exemplo, std::vector<T> requer seu parâmetro para ser um tipo completo, enquanto boost::container::vector<T> não. Às vezes, um tipo completo é exigido somente se você usar determinadas funções de membro; este é o caso para std::unique_ptr<T> , por exemplo.

Um modelo bem documentado deve indicar na sua documentação a todos os requisitos de seus parâmetros, incluindo se eles precisam ser tipos completos ou não.

Outras dicas

A regra principal é que você pode aulas só para a frente-declare cuja memória layout (e, assim, funções de membro e de dados membros) não precisa ser conhecido no arquivo para a frente-declará-lo.

Este descarta classes base e qualquer coisa mas classes usadas através de referências e ponteiros.

Lakos distingue entre o uso de classe

  1. no-name-only (para o qual uma declaração para a frente é suficiente) e
  2. In-size (para o qual é necessária a definição de classe).

Eu nunca vi isso pronunciado de forma mais sucinta:)

Assim como ponteiros e referências a tipos incompletos, você também pode declarar protótipos de função que especificam parâmetros e / ou valores de retorno que são tipos incompletos. No entanto, você não pode definir uma função ter um parâmetro ou retorno tipo que é incompleta, a menos que seja um ponteiro ou referência.

Exemplos:

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types

Nenhuma das respostas até agora descrever quando se pode usar uma declaração para a frente de um modelo de classe. Então, aqui vai.

Um modelo de classe podem ser encaminhadas declarado como:

template <typename> struct X;

A seguir a estrutura do aceita resposta ,

Aqui está o que você pode e não pode fazer.

O que você pode fazer com um tipo incompleto:

  • declarar um membro a ser um ponteiro ou uma referência para o tipo incompleto em outro modelo de classe:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
    
  • declarar um membro a ser um ponteiro ou uma referência a uma das suas exemplificações incompletas:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
    
  • modelos de função Declare ou modelos de função de membro que aceitam / tipos de retorno incompleto:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
    
  • funções declarar ou funções membro que aceitar / um retorno de suas instâncias incompletas:

    void      f1(X<int>);
    X<int>    f2();
    
  • Definir modelos de função ou modelos de função de membro que aceitam / ponteiros de retorno / referências com o modelo incompleto (mas sem o uso de seus membros):

    template <typename T>
       void      f3(X<T>*, X<T>&) {}
    template <typename T>
       X<T>&   f4(X<T>& in) { return in; }
    template <typename T>
       X<T>*   f5(X<T>* in) { return in; }
    
  • Definir funções ou métodos que aceita / ponteiros de retorno / referências a uma de suas instâncias incompletas (mas sem o uso de seus membros):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
    
  • Use-o como uma classe base de uma outra classe de modelo

    template <typename T>
    class Foo : X<T> {} // OK as long as X is defined before
                        // Foo is instantiated.
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
    
  • Use-o para declarar um membro de outro modelo de classe:

    template <typename T>
    class Foo {
        X<T> m; // OK as long as X is defined before
                // Foo is instantiated. 
    };
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
    
  • Definir modelos de função ou métodos que utilizam este tipo

    template <typename T>
      void    f1(X<T> x) {}    // OK if X is defined before calling f1
    template <typename T>
      X<T>    f2(){return X<T>(); }  // OK if X is defined before calling f2
    
    void test1()
    {
       f1(X<int>());  // Compiler error
       f2<int>();     // Compiler error
    }
    
    template <typename T> struct X {};
    
    void test2()
    {
       f1(X<int>());  // OK since X is defined now
       f2<int>();     // OK since X is defined now
    }
    

O que você não pode fazer com um tipo incompleto:

  • Use uma das suas instâncias como uma classe base

    class Foo : X<int> {} // compiler error!
    
  • Use uma de suas instâncias para declarar um membro:

    class Foo {
        X<int> m; // compiler error!
    };
    
  • Definir funções ou métodos usando uma das suas instâncias

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
    
  • Use os métodos ou campos de uma das suas instâncias, na verdade tentando dereference uma variável com tipo incompleto

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
    
  • Criar instâncias explícita da classe template

    template struct X<int>;
    

No arquivo no qual você usa apenas ponteiro ou referência a um class.And nenhuma função membro / membro deve ser invocado pensou aqueles Pointer / referência.

com class Foo; // declaração frente

Podemos declarar membros de dados do tipo Foo * ou Foo &.

Podemos declarar (mas não definir) funções com argumentos, e / ou valores de retorno, do tipo Foo.

Podemos declarar membros de dados estáticos do tipo Foo. Isso ocorre porque os membros de dados estáticos são definidos fora da definição de classe.

Eu estou escrevendo isso como uma resposta em separado ao invés de apenas um comentário, porque eu discordo com a resposta de Luc Touraille, não por razões de legalidade, mas para software robusto e o perigo de má interpretação.

Especificamente, eu tenho um problema com o contrato implícito do que você espera que os usuários de sua interface de ter que saber.

Se você estiver retornando ou aceitar tipos de referência, então você está apenas dizendo que eles podem passar através de um ponteiro ou referência que pode por sua vez ter conhecido apenas através de uma declaração para a frente.

Quando você está retornando um tipo X f2(); incompleta, então você está dizendo que o seu interlocutor deve têm a especificação do tipo cheio de X. Eles precisam dela, a fim de criar o LHS ou objeto temporário no site da chamada.

Da mesma forma, se você aceitar um tipo incompleto, o autor da chamada tem de ter construído o objeto que é o parâmetro. Mesmo se o objeto foi devolvido como outro tipo incompleta a partir de uma função, o site de chamada precisa da declaração completa. ou seja:.

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

Eu acho que há um princípio importante que um cabeçalho deve fornecer informação suficiente para usá-lo sem uma dependência exigindo outros cabeçalhos. Isso significa cabeçalho deve ser capaz de ser incluído em uma unidade de compilação sem causar um erro do compilador quando você usar quaisquer funções que ele declara.

Except

  1. Se esta dependência externa é desejado comportamento. Em vez de usar a compilação condicional você poderia ter um bem documentado exigência para eles para fornecer seu próprio cabeçalho declarando X. Esta é uma alternativa ao uso #ifdefs e pode ser uma maneira útil para introduzir simulações ou outras variantes .

  2. A distinção importante é algumas técnicas modelo onde são explicitamente não se espera que instanciar-los, mencionado apenas para que alguém não conseguir snarky comigo.

A regra geral eu sigo é não incluir qualquer arquivo de cabeçalho a menos que eu preciso. Então, a menos que eu sou armazenar o objeto de uma classe como uma variável membro da minha classe eu não vou incluí-lo, eu só vou usar a declaração para a frente.

Enquanto você não precisa a definição (acho que ponteiros e referências), você pode fugir com declarações para a frente. É por isso que na maior parte você vê-los em cabeçalhos enquanto os arquivos de implementação normalmente vai puxar o cabeçalho para a definição (s) apropriado.

Você normalmente irá querer usar a declaração para a frente em uma aulas arquivo de cabeçalho quando você quiser usar o outro tipo (classe) como um membro da classe. Você não pode usar as classes forward-declarados métodos no cabeçalho do arquivo, porque C ++ não sabe a definição dessa classe nesse ponto ainda. Que de lógica você tem que se deslocar para as CPP-arquivos, mas se você estiver usando o modelo-funções que você deve reduzi-los a apenas a parte que usa o modelo e mover essa função no cabeçalho.

Tome-se que a declaração para a frente vai ter o seu código para compilar (obj é criado). Ligando no entanto (criação exe) não será bem sucedida a menos que as definições são encontrados.

Eu só quero acrescentar uma coisa importante que você pode fazer com uma classe encaminhadas não mencionado na resposta de Luc Touraille.

O que você pode fazer com um tipo incompleto:

Definir funções ou métodos que aceita / retorno ponteiros / referências com o modelo incompleto e para a frente que ponteiros / referências a outra função.

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

Um módulo pode passar através de um objeto de uma classe para a frente declarada para outro módulo.

Como, Luc Touraille já explicou isso muito bem onde usar e não usar a declaração para a frente da classe.

Vou apenas acrescentar a isso por que precisamos usá-lo.

Devemos estar usando declaração para a frente sempre que possível para evitar a injeção de dependência indesejada.

Como arquivos de cabeçalho #include são adicionados em vários arquivos, portanto, se adicionar um cabeçalho para um outro arquivo de cabeçalho que irá adicionar a injeção de dependência indesejada em várias partes do código-fonte que podem ser evitados através da adição de cabeçalho #include em arquivos .cpp sempre que possível, em vez de adicionando a uma outra classe arquivo de cabeçalho e uso declaração para a frente sempre que possível em arquivos .h cabeçalho.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top