c++ - ¿Cuándo puedo usar una declaración hacia adelante?
forward-declaration c++-faq (12)
Además de los punteros y las 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 un 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
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 función miembro por referencia, etc.
En el archivo en el que solo se utiliza el puntero o la referencia a una clase. Y no se debe invocar ninguna función miembro / miembro de ese puntero / referencia.
con class Foo;
// declaración hacia adelante
Podemos declarar miembros de datos del 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.
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 motivos 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 espera que los usuarios de su interfaz tengan que saber.
Si está devolviendo o aceptando tipos de referencia, entonces 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 de reenvío.
Cuando está devolviendo un tipo incompleto X f2();
entonces está diciendo que su interlocutor debe tener la especificación de tipo completa de X. Lo necesitan para crear el objeto temporal o LHS en el sitio de la llamada.
De manera similar, si acepta un tipo incompleto, la persona que llama debe haber construido el objeto que es el parámetro. Incluso si ese objeto fue devuelto como otro tipo incompleto de una función, el sitio de 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 debe poder incluirse en una unidad de compilación sin causar un error de compilación cuando se utiliza cualquier función que declara.
Excepto
Si esta dependencia externa es el comportamiento deseado . En lugar de usar la compilación condicional, podría tener un requisito bien documentado para que suministren su propio encabezado declarando X. Esta es una alternativa al uso de #ifdefs y puede ser una forma útil de presentar simulacros u otras variantes.
La distinción importante son algunas técnicas de plantilla en las que explícitamente NO se espera que las instalen, mencionadas solo para que alguien no se moleste conmigo.
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 de reenvío.
La regla principal es que solo puede reenviar y reenviar las clases cuyo diseño de memoria (y, por lo tanto, las funciones de los miembros y los miembros de los datos) no es necesario que se conozcan en el archivo que lo reenvíe.
Esto descartaría las clases base y cualquier cosa excepto las clases utilizadas a través de referencias y punteros.
Ninguna de las respuestas hasta ahora describe cuándo se puede usar una declaración de reenvío de una plantilla de clase. Entonces, aquí va.
Una plantilla de clase puede ser enviada 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; };
Declare las plantillas de función o las plantillas de función miembro que aceptan / devuelven tipos incompletos:
template <typename T> void f1(X<T>); template <typename T> X<T> f2();
Declare funciones o funciones miembro que acepten / devuelvan una de sus instancias incompletas:
void f1(X<int>); X<int> f2();
Defina plantillas de función o plantillas de función 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; }
Úsalo 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 función o métodos utilizando 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:
Usa 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! };
Define 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, tratando de eliminar la referencia a una variable con un 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>;
Por lo general, deseará usar la declaración de reenvío en un archivo de encabezado de clases cuando desee usar el otro tipo (clase) como miembro de la clase. No puede usar 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 punto. Esa es la lógica que tiene que mover a los archivos .cpp, pero si está usando funciones de plantilla, debe reducirlas solo a la parte que usa la plantilla y mover esa función al encabezado.
Siempre que no necesite la definición (punteros y referencias), puede salirse con las declaraciones de reenvío. Esta es la razón por la que la mayoría los vería en los encabezados, mientras que los archivos de implementación generalmente extraerán el encabezado de las definiciones adecuadas.
Sitúese en la posición del compilador: cuando reenvíe declarar un tipo, todo lo que sabe el compilador es que este tipo existe; no sabe nada sobre su tamaño, miembros o métodos. Por eso 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á conocer el diseño del tipo.
Suponiendo la siguiente declaración hacia adelante.
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 *pt; X &pt; };
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:
Úsalo como una clase base
class Foo : X {} // compiler error!
Úsalo para declarar un miembro:
class Foo { X m; // compiler error! };
Definir funciones o métodos utilizando este tipo.
void f1(X x) {} // compiler error! X f2() {} // compiler error!
Use sus métodos o campos, de hecho tratando de eliminar la referencia de una variable con un tipo incompleto
class Foo { X *m; void method() { m->someMethod(); // compiler error! int i = m->someField; // compiler error! } };
Cuando se trata de plantillas, no hay una regla absoluta: si puede usar un tipo incompleto ya que un 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 que boost::container::vector<T>
no lo hace. A veces, solo se requiere un tipo completo si usa ciertas funciones miembro; Este es el caso de std::unique_ptr<T>
, por ejemplo.
Una plantilla bien documentada debe indicar en su documentación todos los requisitos de sus parámetros, incluyendo si deben ser tipos completos o no.
Solo quiero agregar una cosa importante que pueda 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íelos 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.
Tome en cuenta que la declaración hacia adelante hará que su código se compile (se crea obj). Sin embargo, la vinculación (creación exe) no tendrá éxito a menos que se encuentren las definiciones.
Lakos distingue entre uso de clase
- solo en nombre (para el cual es suficiente una declaración hacia adelante) y
- en tamaño (para lo que se necesita la definición de clase).
Nunca lo he visto pronunciado más sucintamente :)