Quando posso usar uma declaração de encaminhamento?

602

Estou procurando a definição de quando tenho permissão para fazer a declaração de encaminhamento de uma classe no arquivo de cabeçalho de outra classe:

Posso fazê-lo para uma classe base, para uma classe realizada como membro, para uma classe passada para a função de membro por referência, etc.?

Igor Oks
fonte
14
Eu quero desesperadamente este ser renomeado "quando deveria I", e as respostas são atualizadas de forma adequada ...
deworde
12
@deworde Quando você diz quando "deveria", está pedindo uma opinião.
AturSams
@deworde, entendo que você deseja usar declarações avançadas sempre que puder, para melhorar o tempo de construção e evitar referências circulares. A única exceção que consigo pensar é quando um arquivo de inclusão contém typedefs; nesse caso, há uma troca entre redefinir o typedef (e arriscar mudar) e incluir um arquivo inteiro (junto com suas inclusões recursivas).
Ohad Schneider
@OhadSchneider De uma perspectiva prática, eu não sou um grande fã de cabeçalhos que o meu. ÷
deworde
basicamente sempre exigem que você inclua um cabeçalho diferente, a fim de usá-los (para frente decl de parâmetro de construtor é um grande culpado aqui)
deworde

Respostas:

962

Coloque-se na posição do compilador: quando você encaminha um tipo, tudo o 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;

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

O que você pode fazer com um tipo incompleto:

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

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

    void f1(X);
    X    f2();
  • Defina funções ou métodos que aceitam / retornam indicadores / referências ao 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 este tipo

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
  • Use seus métodos ou campos, na verdade tentando desreferenciar 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 parâmetro do 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; esse é o casostd::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 do tipo completo ou não.

Luc Touraille
fonte
4
Ótima resposta, mas veja a minha abaixo para o ponto de engenharia em que discordo. Em resumo, se você não incluir cabeçalhos para tipos incompletos que você aceita ou devolve, força uma dependência invisível de o consumidor do cabeçalho ter que saber de quais outros precisam.
Andy Dent
2
@AndyDent: Verdade, mas o consumidor do cabeçalho precisa incluir apenas as dependências que ele realmente usa, portanto, isso segue o princípio C ++ de "você paga apenas pelo que usa". Mas, de fato, pode ser inconveniente para o usuário que espera que o cabeçalho seja independente.
Luc Touraille
8
Esse conjunto de regras ignora um caso muito importante: você precisa de um tipo completo para instanciar a maioria dos modelos na biblioteca padrão. É necessário prestar atenção especial a isso, porque violar a regra resulta em um comportamento indefinido e pode não causar um erro do compilador.
James Kanze
12
+1 para "coloque-se na posição do compilador". Eu imagino que o "ser compilador" tenha bigode.
PascalVKooten
3
@JesusChrist: Exatamente: quando você passa um objeto por valor, o compilador precisa saber seu tamanho para fazer a manipulação apropriada da pilha; ao passar um ponteiro ou uma referência, o compilador não precisa do tamanho ou layout do objeto, apenas do tamanho de um endereço (isto é, do tamanho de um ponteiro), que não depende do tipo apontado.
Luc Touraille
45

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

Isso excluiria as classes base e qualquer coisa, exceto as classes usadas por referências e ponteiros.

Timo Geusch
fonte
6
Quase. Você também pode se referir a tipos incompletos "simples" (ou seja, sem ponteiro / referência) como parâmetros ou tipos de retorno em protótipos de função.
21139 j_random_hacker 16/02/09
E as classes que eu quero usar como membros de uma classe que eu defino no arquivo de cabeçalho? Posso encaminhar declará-los?
Igor Oks
1
Sim, mas nesse caso você pode usar apenas uma referência ou um ponteiro para a classe declarada a frente. Mas permite que você tenha membros, no entanto.
21909 Reunanen
32

Lakos distingue entre uso de classe

  1. somente em nome (para o qual uma declaração de encaminhamento é suficiente) e
  2. em tamanho (para o qual a definição de classe é necessária).

Eu nunca vi isso pronunciado de forma mais sucinta :)

Marc Mutz - mmutz
fonte
2
O que significa apenas em nome?
Boon
4
@Boon: ouso dizer ...? Se você usa apenas o nome da turma ?
Marc Mutz - mmutz
1
Mais um para Lakos, Marc
mlvljr
28

Além de ponteiros e referências a tipos incompletos, você também pode declarar protótipos de funções que especificam parâmetros e / ou retornam valores que são tipos incompletos. No entanto, você não pode definir uma função com um parâmetro ou tipo de retorno incompleto, 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
j_random_hacker
fonte
19

Até agora, nenhuma das respostas descreve quando é possível usar uma declaração direta de um modelo de classe. Então, aqui vai.

Um modelo de classe pode ser encaminhado declarado como:

template <typename> struct X;

Seguindo a estrutura da resposta aceita ,

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

O que você pode fazer com um tipo incompleto:

  • Declare um membro como um ponteiro ou uma referência ao tipo incompleto em outro modelo de classe:

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

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

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

    void      f1(X<int>);
    X<int>    f2();
  • Defina modelos de função ou modelos de função de membro que aceitem / retornem ponteiros / referências ao tipo incompleto (mas sem usar 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; }
  • Defina funções ou métodos que aceitam / retornam indicadores / referências a uma de suas instanciações incompletas (mas sem usar 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 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 ou métodos de função usando esse 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 de suas instanciações como classe base

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

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

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • Use os métodos ou campos de uma de suas instanciações, de fato tentando desreferenciar uma variável com tipo incompleto

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • Crie instanciações explícitas do modelo de classe

    template struct X<int>;
R Sahu
fonte
2
"Nenhuma das respostas até agora descreve quando é possível a declaração de encaminhamento de um modelo de classe." Não é simplesmente porque a semântica de Xe X<int>é exatamente a mesma, e apenas a sintaxe de declaração direta difere de maneira substantiva, com todas as linhas de uma resposta, exceto uma, equivalendo apenas a Luc e s/X/X<int>/g? Isso é realmente necessário? Ou eu perdi um pequeno detalhe diferente? É possível, mas eu comparados visualmente algumas vezes e não consigo ver nenhuma ...
underscore_d
Obrigado! Essa edição adiciona uma tonelada de informações valiosas. Vou ter que ler várias vezes para entendê-lo completamente ... ou talvez usar a tática muitas vezes melhor de esperar até que eu fique terrivelmente confuso em código real e voltando aqui! Suspeito que poderei usar isso para reduzir dependências em vários lugares.
Underscore_d
4

No arquivo em que você usa apenas ponteiro ou referência a uma classe. E nenhuma função de membro / membro deve ser chamada por meio desses ponteiros / referências.

com class Foo;// declaração a termo

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.

yesraaj
fonte
4

Estou escrevendo isso como uma resposta separada, e não apenas um comentário, porque discordo da resposta de Luc Touraille, não com base na legalidade, mas em 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 tenham que saber.

Se você estiver retornando ou aceitando tipos de referência, estará apenas dizendo que eles podem passar por um ponteiro ou referência que, por sua vez, eles podem ter conhecido apenas por meio de uma declaração direta.

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

Da mesma forma, se você aceitar um tipo incompleto, o chamador deverá 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. 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 existe um princípio importante de que um cabeçalho deve fornecer informações suficientes para usá-lo sem uma dependência que exija outros cabeçalhos. Isso significa que o cabeçalho deve poder ser incluído em uma unidade de compilação sem causar um erro no compilador quando você usa as funções declaradas.

Exceto

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

  2. A distinção importante são algumas técnicas de modelo nas quais você NÃO deve explicitamente instancia-las, mencionadas apenas para que alguém não fique irritado comigo.

Andy Dent
fonte
"Acho que existe um princípio importante de que um cabeçalho deve fornecer informações suficientes para usá-lo sem uma dependência que exija outros cabeçalhos". - outra questão é mencionada em um comentário de Adrian McCarthy na resposta de Naveen. Isso fornece uma boa razão para não seguir o princípio "deve fornecer informações suficientes para usar", mesmo para os tipos atualmente não modelados.
Tony Delroy
3
Você está falando sobre quando deve (ou não) usar a declaração direta. Esse não é totalmente o objetivo dessa questão. Trata-se de conhecer as possibilidades técnicas quando (por exemplo) deseja resolver um problema de dependência circular.
JonnyJD
1
I disagree with Luc Touraille's answerEntão, escreva um comentário para ele, incluindo um link para uma postagem no blog, se precisar. Isso não responde à pergunta. Se todo mundo pensasse em perguntas sobre como o X funciona, respostas justificadas discordariam dele ou debateriam limites dentro dos quais deveríamos restringir nossa liberdade de usar o X - quase não teríamos respostas reais.
Underscore_d
3

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

Naveen
fonte
2
Isso quebra o encapsulamento e torna o código quebradiço. Para fazer isso, você precisa saber se o tipo é um typedef ou uma classe para um modelo de classe com parâmetros de modelo padrão e, se a implementação mudar, você precisará atualizar sempre que colocar uma declaração de encaminhamento.
Adrian McCarthy
O @AdrianMcCarthy está certo, e uma solução razoável é ter um cabeçalho de declaração de encaminhamento incluído no cabeçalho cujo conteúdo ele declara encaminhado, que deve ser de propriedade / mantido / enviado por quem possuir esse cabeçalho. Por exemplo: o cabeçalho da biblioteca iosfwd Standard, que contém declarações de encaminhamento de conteúdo iostream.
Tony Delroy
3

Contanto que você não precise da definição (pense em indicadores e referências), você pode se safar das declarações avançadas. É por isso que você os vê principalmente nos cabeçalhos, enquanto os arquivos de implementação normalmente puxam o cabeçalho para as definições apropriadas.

dirkgently
fonte
0

Você geralmente desejará usar a declaração direta 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 declaradas a frente no arquivo de cabeçalho porque o C ++ ainda não conhece a definição dessa classe naquele momento. Essa é a lógica que você precisa mover para os arquivos .cpp, mas se estiver usando funções de modelo, reduza-as apenas à parte que usa o modelo e mova essa função para o cabeçalho.

Patrick Glandien
fonte
Isso não faz sentido. Não se pode ter um membro de um tipo incompleto. A declaração de qualquer classe deve fornecer tudo o que todos os usuários precisam saber sobre seu tamanho e layout. Seu tamanho inclui os tamanhos de todos os seus membros não estáticos. A declaração antecipada de um membro deixa os usuários sem ideia do seu tamanho.
underscore_d
0

Suponha que a declaração direta obtenha seu código para compilar (obj é criado). No entanto, a vinculação (criação de exe) não terá êxito, a menos que as definições sejam encontradas.

Sesh
fonte
2
Por que duas pessoas aprovaram isso? Você não está falando sobre o que a pergunta está falando. Você quer dizer uma declaração de funções normal - não direta - . A questão é sobre a declaração direta de classes . Como você disse "a declaração direta encaminhará seu código", faça-me um favor: compile class A; class B { A a; }; int main(){}e deixe-me saber como isso acontece. Claro que não será compilado. Todas as respostas adequadas aqui explicam o porquê e os contextos precisos e limitados nos quais a declaração antecipada é válida. Você escreveu isso sobre algo totalmente diferente.
underscore_d
0

Eu só quero acrescentar 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:

Defina funções ou métodos que aceitem / retornem ponteiros / referências para o tipo incompleto e encaminhem 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 a frente para outro módulo.

Bom homem
fonte
"uma classe encaminhada" e "uma classe declarada encaminhada" podem estar enganados em se referir a duas coisas muito diferentes. O que você escreveu segue diretamente de conceitos implícitos na resposta de Luc, portanto, embora ele tenha feito um bom comentário adicionando esclarecimentos claros, não tenho certeza se justifica uma resposta.
Underscore_d
0

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

Vou acrescentar a isso porque precisamos usá-lo.

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

Como os #includearquivos de cabeçalho são adicionados em vários arquivos, portanto, se adicionarmos um cabeçalho em outro arquivo de cabeçalho, ele adicionará injeção de dependência indesejada em várias partes do código-fonte, o que pode ser evitado adicionando #includecabeçalho nos .cpparquivos sempre que possível, em vez de adicionar a outro arquivo de cabeçalho e use a declaração de encaminhamento de classe sempre que possível nos .harquivos de cabeçalho .

A 786
fonte