c++ - tipos - Quando posso usar uma declaração para frente?



include cpp in header template (8)

A regra geral que eu sigo é não incluir nenhum arquivo de cabeçalho, a menos que seja necessário. Então, a menos que eu esteja armazenando o objeto de uma classe como uma variável de membro da minha classe, eu não a incluirei, apenas usarei a declaração de encaminhamento.

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

Posso fazer isso para uma classe base, para uma classe mantida como membro, para uma classe passada para a função de membro por referência, etc.?


Answer #1

A regra principal é que você só pode encaminhar classes de declaração cujo layout de memória (e, portanto, funções membro e membros de dados) não precisam ser conhecidos no arquivo que você o declara para frente.

Isso descartaria classes base e qualquer coisa além de classes usadas por meio de referências e ponteiros.


Answer #2

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 retornam valores que são tipos incompletos. No entanto, você não pode definir uma função que tenha um parâmetro ou um tipo de retorno que esteja incompleto, a menos que seja um ponteiro ou uma 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

Answer #3

Coloque-se na posição do compilador: quando você encaminha declare um tipo, tudo que o compilador sabe é que esse tipo existe; não sabe nada sobre seu tamanho, membros ou métodos. É por isso que é chamado de tipo incompleto . Portanto, você não pode usar o tipo para declarar um membro ou uma classe base, pois o compilador precisaria conhecer o layout do tipo.

Assumindo a seguinte declaração antecipada.

class X;

Veja o que você pode e não pode fazer.

O que você pode fazer com um tipo incompleto:

  • Declare um membro para ser um ponteiro ou uma referência ao tipo incompleto:

    class Foo {
        X *pt;
        X &pt;
    };
  • Declare funções ou métodos que aceitam / retornam tipos incompletos:

    void f1(X);
    X    f2();
  • Definir funções ou métodos que aceitam / retornam ponteiros / referências para o tipo incompleto (mas sem usar 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 usando esse tipo

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
  • Use seus métodos ou campos, na verdade, tentando cancelar a referência de 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 de modelo depende da maneira como o tipo é usado no modelo.

Por exemplo, std::vector<T> requer que seu parâmetro seja um tipo completo, enquanto boost::container::vector<T> não. Às vezes, um tipo completo é necessário apenas se você usar determinadas funções de membro; Este é o caso de std::unique_ptr<T> , por exemplo.

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


Answer #4

Estou escrevendo isso como uma resposta à parte, em vez de apenas um comentário, porque não concordo com a resposta de Luc Touraille, não com base na legalidade, mas no software robusto e no perigo de erros de interpretação.

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

Se você está retornando ou aceitando tipos de referência, então você está apenas dizendo que eles podem passar por um ponteiro ou referência que eles podem, por sua vez, saber apenas através de uma declaração direta.

Quando você está retornando um tipo incompleto X f2(); então você está dizendo que seu chamador deve ter a especificação de tipo completa de X. Eles precisam dele para criar o LHS ou objeto temporário no site da chamada.

Da mesma forma, se você aceitar um tipo incompleto, o chamador deve ter construído o objeto que é o parâmetro. Mesmo se esse objeto foi retornado como outro tipo incompleto de uma função, o site de chamada precisa da declaração completa. ie:

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ções suficientes para usá-lo sem uma dependência que exige outros cabeçalhos. Isso significa que o cabeçalho deve poder ser incluído em uma unidade de compilação sem causar um erro do compilador quando você usa qualquer função declarada.

Exceto

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

  2. A distinção importante é algumas técnicas de modelo em que você não é explicitamente esperado para instanciá-las, mencionado apenas para que alguém não fique nervoso comigo.


Answer #5

Eu só quero adicionar uma coisa importante que você pode fazer com uma classe encaminhada não mencionada na resposta de Luc Touraille.

O que você pode fazer com um tipo incompleto:

Definir funções ou métodos que aceitam / retornam ponteiros / referências para o tipo incompleto e encaminham esses ponteiros / referências para 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 por um objeto de uma classe declarada para frente para outro módulo.


Answer #6

No arquivo em que você usa apenas o ponteiro ou a referência a uma classe. E nenhuma função de membro / membro deve ser invocada com base naqueles Ponteiro / referência.

com class Foo; // declaração antecipada

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 da classe.


Answer #7

Você geralmente desejará usar a declaração de encaminhamento em um arquivo de cabeçalho de classes quando desejar usar o outro tipo (classe) como membro da classe. Você não pode usar os métodos de classes forward-declarados no arquivo de cabeçalho porque C ++ não conhece a definição dessa classe nesse momento ainda. Isso é lógica, você tem que passar para os arquivos .cpp, mas se você estiver usando template-functions você deve reduzi-los para apenas a parte que usa o modelo e mover essa função para o cabeçalho.





c++-faq