¿Cuándo puedo usar una declaración adelantada?

602

Estoy buscando la definición de cuándo se me permite hacer una declaración hacia adelante de una clase en el archivo de encabezado de otra clase:

¿Se me permite hacerlo para una clase base, para una clase mantenida como miembro, para una clase pasada a la función miembro por referencia, etc.?

Igor Oks
fuente
14
Desesperadamente quiero que esto sea a llamarse "cuando debería yo", y las respuestas actualizado apropiadamente ...
deworde
12
@deworde Cuando dices cuándo "debería" estás pidiendo opinión.
AturSams
@deworde, entiendo que desea utilizar declaraciones de reenvío siempre que pueda, para mejorar el tiempo de compilación y evitar referencias circulares. La única excepción que se me ocurre es cuando un archivo de inclusión contiene typedefs, en cuyo caso hay una compensación entre redefinir el typedef (y arriesgarse a cambiarlo) e incluir un archivo completo (junto con sus recursivos incluye).
Ohad Schneider
@OhadSchneider Desde una perspectiva práctica, no soy un gran admirador de los encabezados que mi. ÷
deworde
básicamente siempre requieren que incluir una cabecera diferente con el fin de utilizarlos (avance decl del parámetro constructor es un gran culpable aquí)
deworde

Respuestas:

962

Póngase en la posición del compilador: cuando reenvía declarar un tipo, todo lo que el compilador sabe es que este tipo existe; no sabe nada sobre su tamaño, miembros o métodos. Es por eso que se llama un tipo incompleto . Por lo tanto, no puede usar el tipo para declarar un miembro o una clase base, ya que el compilador necesitaría conocer el diseño del tipo.

Asumiendo la siguiente declaración adelantada.

class X;

Esto es lo que puedes y no puedes hacer.

Lo que puedes hacer con un tipo incompleto:

  • Declare que un miembro es un puntero o una referencia al tipo incompleto:

    class Foo {
        X *p;
        X &r;
    };
    
  • Declarar funciones o métodos que aceptan / devuelven tipos incompletos:

    void f1(X);
    X    f2();
    
  • Defina funciones o métodos que acepten / devuelvan punteros / referencias al tipo incompleto (pero sin usar sus miembros):

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

Lo que no puedes hacer con un tipo incompleto:

  • Úselo como una clase base

    class Foo : X {} // compiler error!
  • Úselo para declarar un miembro:

    class Foo {
        X m; // compiler error!
    };
    
  • Definir funciones o métodos con este tipo.

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • Utilice sus métodos o campos, de hecho, intente desreferenciar una variable con tipo incompleto

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

Cuando se trata de plantillas, no existe una regla absoluta: si puede usar un tipo incompleto como parámetro de plantilla depende de la forma en que se usa el tipo en la plantilla.

Por ejemplo, std::vector<T>requiere que su parámetro sea un tipo completo, mientras boost::container::vector<T>que no. A veces, se requiere un tipo completo solo si utiliza ciertas funciones miembro; Este es el casostd::unique_ptr<T> , por ejemplo.

Una plantilla bien documentada debe indicar en su documentación todos los requisitos de sus parámetros, incluso si deben ser tipos completos o no.

Luc Touraille
fuente
44
Gran respuesta, pero mira la mía a continuación para ver el punto de ingeniería en el que no estoy de acuerdo. En resumen, si no incluye encabezados para los tipos incompletos que acepta o devuelve, fuerza una dependencia invisible en el consumidor de su encabezado para saber qué otros necesitan.
Andy Dent
2
@AndyDent: Es cierto, pero el consumidor del encabezado solo necesita incluir las dependencias que realmente usa, por lo que esto sigue el principio de C ++ de "solo paga por lo que usa". Pero, de hecho, puede ser inconveniente para el usuario que esperaría que el encabezado sea independiente.
Luc Touraille
8
Este conjunto de reglas ignora un caso muy importante: necesita un tipo completo para crear instancias de la mayoría de las plantillas en la biblioteca estándar. Se debe prestar especial atención a esto, porque violar la regla da como resultado un comportamiento indefinido y no puede causar un error del compilador.
James Kanze
12
+1 para el "ponte en la posición del compilador". Me imagino que el "ser compilador" tiene bigote.
PascalVKooten
3
@JesusChrist: Exactamente: cuando pasa un objeto por valor, el compilador necesita saber su tamaño para realizar la manipulación de pila adecuada; al pasar un puntero o una referencia, el compilador no necesita el tamaño o el diseño del objeto, solo el tamaño de una dirección (es decir, el tamaño de un puntero), que no depende del tipo señalado.
Luc Touraille
45

La regla principal es que solo puede declarar hacia adelante las clases cuyo diseño de memoria (y, por lo tanto, las funciones miembro y los miembros de datos) no necesitan ser conocidos en el archivo que lo declara hacia adelante.

Esto descartaría las clases base y cualquier otra cosa que no sean las clases utilizadas a través de referencias y punteros.

Timo Geusch
fuente
66
Casi. También puede referirse a los tipos incompletos "sin formato / referencia" como parámetros o tipos de retorno en prototipos de funciones.
j_random_hacker
¿Qué pasa con las clases que quiero usar como miembros de una clase que defino en el archivo de encabezado? ¿Puedo reenviarlos?
Igor Oks
1
Sí, pero en ese caso solo puede usar una referencia o un puntero a la clase declarada hacia adelante. Pero te permite tener miembros, sin embargo.
Reunanen
32

Lakos distingue entre uso de clase

  1. solo en nombre (para lo cual una declaración directa es suficiente) y
  2. en tamaño (para el cual se necesita la definición de clase).

Nunca lo he visto pronunciado de manera más sucinta :)

Marc Mutz - mmutz
fuente
2
¿Qué significa solo en nombre?
Boon
44
@Boon: ¿me atrevo a decirlo ...? Si usa solo el nombre de la clase ?
Marc Mutz - mmutz
1
Más uno para Lakos, Marc
mlvljr
28

Además de punteros y referencias a tipos incompletos, también puede declarar prototipos de funciones que especifican parámetros y / o valores de retorno que son tipos incompletos. Sin embargo, no puede definir una función que tenga un parámetro o tipo de retorno que esté incompleto, a menos que sea un puntero o una referencia.

Ejemplos:

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
fuente
19

Ninguna de las respuestas hasta ahora describe cuándo se puede usar una declaración directa de una plantilla de clase. Entonces, aquí va.

Se puede reenviar una plantilla de clase declarada como:

template <typename> struct X;

Siguiendo la estructura de la respuesta aceptada ,

Esto es lo que puedes y no puedes hacer.

Lo que puedes hacer con un tipo incompleto:

  • Declare que un miembro es un puntero o una referencia al tipo incompleto en otra plantilla de clase:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
  • Declare que un miembro es un puntero o una referencia a una de sus instancias incompletas:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • Declarar plantillas de funciones o plantillas de funciones miembro que aceptan / devuelven tipos incompletos:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • Declarar funciones o funciones miembro que aceptan / devuelven una de sus instancias incompletas:

    void      f1(X<int>);
    X<int>    f2();
  • Defina plantillas de funciones o plantillas de funciones miembro que acepten / devuelvan punteros / referencias al tipo incompleto (pero sin usar sus miembros):

    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 funciones o métodos que acepten / devuelvan punteros / referencias a una de sus instancias incompletas (pero sin usar sus miembros):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
  • Úselo como una clase base de otra clase de plantilla

    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.
  • Úselo para declarar un miembro de otra plantilla de clase:

    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 plantillas de funciones o métodos usando 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
    }

Lo que no puedes hacer con un tipo incompleto:

  • Use una de sus instancias como clase base

    class Foo : X<int> {} // compiler error!
  • Use una de sus instancias para declarar un miembro:

    class Foo {
        X<int> m; // compiler error!
    };
  • Definir funciones o métodos utilizando una de sus instancias.

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • Utilice los métodos o campos de una de sus instancias, de hecho, intente desreferenciar una variable con tipo incompleto

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • Crear instancias explícitas de la plantilla de clase.

    template struct X<int>;
R Sahu
fuente
2
"Ninguna de las respuestas hasta ahora describe cuándo se puede hacer la declaración de una plantilla de clase". ¿No es eso simplemente porque la semántica de Xy X<int>es exactamente la misma, y ​​solo la sintaxis de declaración hacia adelante difiere de alguna manera sustantiva, con todas menos una línea de su respuesta que equivale a tomar Luc y s/X/X<int>/g? ¿Es eso realmente necesario? ¿O me he perdido un pequeño detalle que es diferente? Es posible, pero lo he comparado visualmente varias veces y no puedo ver ninguno ...
underscore_d
¡Gracias! Esa edición agrega una tonelada de información valiosa. Tendré que leerlo varias veces para comprenderlo completamente ... ¡o tal vez usar la táctica de esperar a menudo mejor hasta que me confunda horriblemente con el código real y vuelva aquí! Sospecho que podré usar esto para reducir las dependencias en varios lugares.
underscore_d
4

En el archivo en el que usa solo el puntero o la referencia a una clase.

con class Foo;// declaración hacia adelante

Podemos declarar miembros de datos de tipo Foo * o Foo &.

Podemos declarar (pero no definir) funciones con argumentos, y / o valores de retorno, de tipo Foo.

Podemos declarar miembros de datos estáticos de tipo Foo. Esto se debe a que los miembros de datos estáticos se definen fuera de la definición de clase.

yesraaj
fuente
4

Escribo esto como una respuesta separada en lugar de solo un comentario porque no estoy de acuerdo con la respuesta de Luc Touraille, no por razones de legalidad, sino por un software robusto y el peligro de una mala interpretación.

Específicamente, tengo un problema con el contrato implícito de lo que usted espera que los usuarios de su interfaz tengan que saber.

Si está devolviendo o aceptando tipos de referencia, solo está diciendo que pueden pasar a través de un puntero o referencia que a su vez pueden haber conocido solo a través de una declaración directa.

Cuando devuelve un tipo incompleto X f2();, está diciendo que la persona que llama debe tener la especificación de tipo completo de X. La necesita para crear el LHS u objeto temporal en el sitio de la llamada.

Del mismo modo, si acepta un tipo incompleto, la persona que llama debe haber construido el objeto que es el parámetro. Incluso si ese objeto se devolvió como otro tipo incompleto de una función, el sitio de la llamada necesita la declaración completa. es decir:

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

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

Creo que hay un principio importante de que un encabezado debe proporcionar suficiente información para usarlo sin una dependencia que requiera otros encabezados. Eso significa que el encabezado debería poder incluirse en una unidad de compilación sin causar un error de compilación cuando utilice cualquier función que declare.

Excepto

  1. Si esta dependencia externa es el comportamiento deseado . En lugar de utilizar la compilación condicional, podría tener un requisito bien documentado para que proporcionen su propio encabezado declarando X. Esta es una alternativa al uso de #ifdefs y puede ser una forma útil de introducir simulacros u otras variantes.

  2. La distinción importante son algunas técnicas de plantilla en las que NO se espera explícitamente que las instancia, mencionada solo para que alguien no se vuelva sarcástico conmigo.

Andy Dent
fuente
"Creo que hay un principio importante de que un encabezado debe proporcionar suficiente información para usarlo sin una dependencia que requiera otros encabezados". - Otro tema se menciona en un comentario de Adrian McCarthy sobre la respuesta de Naveen. Eso proporciona una buena razón para no seguir su principio de "debe proporcionar suficiente información para usar" incluso para los tipos que actualmente no tienen plantillas.
Tony Delroy
3
Estás hablando de cuándo deberías (o no deberías) usar la declaración hacia adelante. Sin embargo, ese no es el punto de esta pregunta. Se trata de conocer las posibilidades técnicas cuando (por ejemplo) quiere romper un problema de dependencia circular.
JonnyJD
1
I disagree with Luc Touraille's answerAsí que escríbele un comentario, incluyendo un enlace a una publicación de blog si necesitas la extensión. Esto no responde la pregunta formulada. Si todos pensaran en preguntas sobre cómo funciona X, las respuestas justificadas no están de acuerdo con que X haga eso o debatan límites dentro de los cuales deberíamos restringir nuestra libertad de usar X, casi no tendríamos respuestas reales.
underscore_d
3

La regla general que sigo es no incluir ningún archivo de encabezado a menos que tenga que hacerlo. Entonces, a menos que esté almacenando el objeto de una clase como una variable miembro de mi clase, no lo incluiré, solo usaré la declaración directa.

Naveen
fuente
2
Esto rompe la encapsulación y hace que el código sea frágil. Para hacer esto, debe saber si el tipo es un typedef o una clase para una plantilla de clase con parámetros de plantilla predeterminados, y si la implementación alguna vez cambia, deberá actualizar cada vez que haya utilizado una declaración de reenvío.
Adrian McCarthy
@AdrianMcCarthy tiene razón, y una solución razonable es tener un encabezado de declaración de reenvío que esté incluido por el encabezado cuyo contenido declare, que también debe ser propiedad / mantenido / enviado por quien posee ese encabezado. Por ejemplo: el encabezado de la biblioteca estándar iosfwd, que contiene declaraciones directas de contenido iostream.
Tony Delroy
3

Siempre que no necesite la definición (piense en punteros y referencias), puede salirse con la suya. Es por eso que en su mayoría los vería en encabezados, mientras que los archivos de implementación generalmente extraerán el encabezado para las definiciones adecuadas.

Dirkgently
fuente
0

Por lo general, querrá usar la declaración directa en un archivo de encabezado de clases cuando desee usar el otro tipo (clase) como miembro de la clase. No puede utilizar los métodos de clases declaradas hacia adelante en el archivo de encabezado porque C ++ aún no conoce la definición de esa clase en ese momento. Esa es la lógica que tiene que pasar a los archivos .cpp, pero si está utilizando funciones de plantilla, debe reducirlas solo a la parte que usa la plantilla y mover esa función al encabezado.

Patrick Glandien
fuente
Esto no tiene sentido. Uno no puede tener un miembro de un tipo incompleto. La declaración de cualquier clase debe proporcionar todo lo que todos los usuarios necesitan saber sobre su tamaño y diseño. Su tamaño incluye los tamaños de todos sus miembros no estáticos. La declaración directa de un miembro deja a los usuarios sin tener idea de su tamaño.
underscore_d
0

Supongamos que la declaración hacia adelante hará que su código se compile (se crea obj). Sin embargo, la vinculación (creación de exe) no tendrá éxito a menos que se encuentren las definiciones.

Sesh
fuente
2
¿Por qué alguna vez 2 personas votaron por esto? No estás hablando de lo que está hablando la pregunta. Te refieres a la declaración normal de funciones , no hacia adelante . La pregunta es sobre la declaración directa de clases . Como dijiste "la declaración de avance hará que tu código se compile", hazme un favor: compila class A; class B { A a; }; int main(){}y hazme saber cómo funciona. Por supuesto que no se compilará. Todas las respuestas adecuadas aquí explican por qué y los contextos precisos y limitados en los que la declaración directa es válida. En cambio, has escrito esto sobre algo totalmente diferente.
underscore_d
0

Solo quiero agregar una cosa importante que puede hacer con una clase reenviada que no se menciona en la respuesta de Luc Touraille.

Lo que puedes hacer con un tipo incompleto:

Defina funciones o métodos que acepten / devuelvan punteros / referencias al tipo incompleto y reenvíen esos punteros / referencias a otra función.

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

Un módulo puede pasar a través de un objeto de una clase declarada hacia adelante a otro módulo.

Hombre agradable
fuente
"una clase reenviada" y "una clase reenviada declarada" podrían confundirse con dos cosas muy diferentes. Lo que ha escrito se deduce directamente de los conceptos implícitos en la respuesta de Luc, por lo que si bien habría hecho un buen comentario agregando una aclaración abierta, no estoy seguro de que justifique una respuesta.
underscore_d
0

Como, Luc Touraille ya lo ha explicado muy bien dónde usar y no usar la declaración hacia adelante de la clase.

Solo agregaré a eso por qué necesitamos usarlo.

Deberíamos utilizar la declaración de reenvío siempre que sea posible para evitar la inyección de dependencia no deseada.

Como #includelos archivos de encabezado se agregan en varios archivos, por lo tanto, si agregamos un encabezado en otro archivo de encabezado, se agregará una inyección de dependencia no deseada en varias partes del código fuente que se puede evitar agregando #includeencabezado a los .cpparchivos siempre que sea posible en lugar de agregarlo a otro archivo de encabezado y utilice la declaración de avance de clase siempre que sea posible en los .harchivos de encabezado

A 786
fuente