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.?
c++
forward-declaration
c++-faq
Igor Oks
fuente
fuente
Respuestas:
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.
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:
Declarar funciones o métodos que aceptan / devuelven tipos incompletos:
Defina funciones o métodos que acepten / devuelvan punteros / referencias al tipo incompleto (pero sin usar sus miembros):
Lo que no puedes hacer con un tipo incompleto:
Úselo como una clase base
Úselo para declarar un miembro:
Definir funciones o métodos con este tipo.
Utilice sus métodos o campos, de hecho, intente desreferenciar una variable con tipo incompleto
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, mientrasboost::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.
fuente
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.
fuente
Lakos distingue entre uso de clase
Nunca lo he visto pronunciado de manera más sucinta :)
fuente
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:
fuente
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:
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:
Declare que un miembro es un puntero o una referencia a una de sus instancias incompletas:
Declarar plantillas de funciones o plantillas de funciones miembro que aceptan / devuelven tipos incompletos:
Declarar funciones o funciones miembro que aceptan / devuelven una de sus instancias incompletas:
Defina plantillas de funciones o plantillas de funciones miembro que acepten / devuelvan punteros / referencias al tipo incompleto (pero sin usar sus miembros):
Defina funciones o métodos que acepten / devuelvan punteros / referencias a una de sus instancias incompletas (pero sin usar sus miembros):
Úselo como una clase base de otra clase de plantilla
Úselo para declarar un miembro de otra plantilla de clase:
Definir plantillas de funciones o métodos usando este tipo
Lo que no puedes hacer con un tipo incompleto:
Use una de sus instancias como clase base
Use una de sus instancias para declarar un miembro:
Definir funciones o métodos utilizando una de sus instancias.
Utilice los métodos o campos de una de sus instancias, de hecho, intente desreferenciar una variable con tipo incompleto
Crear instancias explícitas de la plantilla de clase.
fuente
X
yX<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 ys/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 ...En el archivo en el que usa solo el puntero o la referencia a una clase.
con
class Foo;
// declaración hacia adelantePodemos 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.
fuente
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:
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
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.
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.
fuente
I disagree with Luc Touraille's answer
Así 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.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.
fuente
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.
fuente
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.
fuente
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.
fuente
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.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.
Un módulo puede pasar a través de un objeto de una clase declarada hacia adelante a otro módulo.
fuente
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
#include
los 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#include
encabezado a los.cpp
archivos 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.h
archivos de encabezadofuente