Quando posso usar uma declaração direta?

Eu estou procurando a definição de quando estou autorizado a fazer declaração de encaminhamento de uma class no arquivo de header de outra class:

Posso fazer isso para uma class base, para uma class mantida como membro, para uma class passada para a function de membro por referência, etc.?

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 methods. É por isso que é chamado de tipo incompleto . Portanto, você não pode usar o tipo para declarar um membro ou uma class 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 methods que aceitam / retornam tipos incompletos:

     void f1(X); X f2(); 
  • Definir funções ou methods que aceitam / retornam pointers / 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 class base

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

     class Foo { X m; // compiler error! }; 
  • Definir funções ou methods usando esse tipo

     void f1(X x) {} // compiler error! X f2() {} // compiler error! 
  • Use seus methods 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 há 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 requer que seu parâmetro seja um tipo completo, enquanto boost::container::vector não. Às vezes, um tipo completo é necessário somente se você usar determinadas funções de membro; Este é o caso de std::unique_ptr , 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.

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

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

Lakos distingue entre uso de class

  1. apenas no nome (para o qual uma declaração prévia é suficiente) e
  2. em tamanho (para o qual a definição de class é necessária).

Eu nunca vi isso pronunciado mais sucintamente 🙂

Assim como pointers e referências a tipos incompletos, você também pode declarar protótipos de function que especificam parâmetros e / ou retornam valores que são tipos incompletos. No entanto, você não pode definir uma function com 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 

Nenhuma das respostas até agora descreve quando se pode usar uma declaração direta de um modelo de class. Então, aqui vai.

Um modelo de class pode ser encaminhado declarado como:

 template  struct X; 

Seguindo a estrutura da resposta aceita ,

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

O que você pode fazer com um tipo incompleto:

  • Declare que um membro seja um ponteiro ou uma referência ao tipo incompleto em outro modelo de class:

     template  class Foo { X* ptr; X& ref; }; 
  • Declare um membro para ser um ponteiro ou uma referência a uma de suas instanciações incompletas:

     class Foo { X* ptr; X& ref; }; 
  • Declare modelos de function ou modelos de function de membro que aceitam / retornam tipos incompletos:

     template  void f1(X); template  X f2(); 
  • Declare funções ou funções de membro que aceitam / retornam uma de suas instanciações incompletas:

     void f1(X); X f2(); 
  • Definir modelos de function ou modelos de function de membro que aceitam / retornam pointers / referências para o tipo incompleto (mas sem usar seus membros):

     template  void f3(X*, X&) {} template  X& f4(X& in) { return in; } template  X* f5(X* in) { return in; } 
  • Definir funções ou methods que aceitam / retornam pointers / referências para uma de suas instanciações incompletas (mas sem usar seus membros):

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

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

     template  class Foo { X m; // OK as long as X is defined before // Foo is instantiated. }; Foo a1; // Compiler error. template  struct X {}; Foo a2; // OK since X is now defined. 
  • Definir modelos de function ou methods usando esse tipo

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

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

  • Use uma de suas instanciações como uma class base

     class Foo : X {} // compiler error! 
  • Use uma de suas instanciações para declarar um membro:

     class Foo { X m; // compiler error! }; 
  • Definir funções ou methods usando uma de suas instanciações

     void f1(X x) {} // compiler error! X f2() {return X(); } // compiler error! 
  • Use os methods ou campos de uma de suas instanciações, 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! } }; 
  • Criar instanciações explícitas do modelo de class

     template struct X; 

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

com class Foo; // declaração para 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 da class.

Contanto que você não precise da definição (pense em pointers e referências), você pode se safar com as declarações futuras. É por isso que na maioria das vezes você os veria nos headers enquanto os arquivos de implementação normalmente puxam o header para a (s) definição (s) apropriada (s).

A regra geral que eu sigo é não include nenhum arquivo de header a menos que seja necessário. Então, a menos que eu esteja armazenando o object de uma class como uma variável de membro da minha class, eu não a includeei, apenas usarei a declaração de encaminhamento.

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 interpretações errôneas.

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 através de um ponteiro ou referência que eles podem, por sua vez, ter conhecido apenas através de uma declaração antecipada.

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 object temporário no site da chamada.

Da mesma forma, se você aceitar um tipo incompleto, o chamador deve ter construído o object que é o parâmetro. Mesmo se esse object foi retornado como outro tipo incompleto de uma function, 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 header deve fornecer informações suficientes para usá-lo sem uma dependência que exige outros headers. Isso significa que o header deve poder ser incluído em uma unidade de compilation sem causar um erro do compilador quando você usa qualquer function declarada.

Exceto

  1. Se esta dependência externa é o comportamento desejado . Em vez de usar a compilation condicional, você poderia ter um requisito bem documentado para fornecer seu próprio header 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, mencionadas apenas para que alguém não fique nervoso comigo.

Geralmente, você desejará usar a declaração de encaminhamento em um arquivo de header de classs quando quiser usar o outro tipo (class) como membro da class. Você não pode usar os methods de classs declaradas para frente no arquivo de header porque o C ++ ainda não conhece a definição dessa class naquele momento. 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 function para o header.

Aceite que a declaração de encaminhamento receberá seu código para compilar (obj é criado). No entanto, vincular (criação de exe) não será bem-sucedido a menos que as definições sejam encontradas.

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

O que você pode fazer com um tipo incompleto:

Definir funções ou methods que aceitam / retornam pointers / referências para o tipo incompleto e encaminham esses pointers / referências para outra function.

 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 object de uma class declarada para frente para outro módulo.