Pregunta ¿Cuándo puedo usar una declaración directa?


Estoy buscando la definición de cuándo puedo exportar la declaración de una clase en el archivo de encabezado de otra clase:

¿Se me permite hacerlo para una clase base, para una clase celebrada como miembro, para una clase que se transfiere a la función miembro por referencia, etc.?


514
2018-02-16 15:31


origen


Respuestas:


Póngase en la posición del compilador: cuando reenvía declara 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 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 directa.

class X;

Esto es lo que puedes y no puedes hacer.

Qué 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();
    
  • Definir funciones o métodos que aceptan / devuelven 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 que utilizan este tipo

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • Usa sus métodos o campos, de hecho, intenta 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> no. Algunas veces, se requiere un tipo completo solo si usa ciertas funciones de 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, incluso si deben ser tipos completos o no.


838
2018-02-16 16:24



La regla principal es que solo puede reenviar-declarar clases cuyo diseño de memoria (y por lo tanto funciones de miembros y miembros de datos) no necesitan ser conocidos en el archivo que reenvía-declararlo.

Esto descartaría las clases base y todo lo que no sean clases utilizadas a través de referencias y punteros.


37
2018-02-16 15:35



Lakos distingue entre uso de clase

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

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


30
2017-07-21 07:44



Además de los punteros y las referencias a tipos incompletos, también puede declarar prototipos de funciones que especifiquen parámetros y / o valores de retorno que sean tipos incompletos. Sin embargo, no puedes definir una función que tiene un parámetro o tipo de retorno que está incompleto, a menos que sea un puntero o 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

24
2018-02-16 16:03



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

Una plantilla de clase puede reenviarse declarada como:

template <typename> struct X;

Siguiendo la estructura del respuesta aceptada,

Esto es lo que puedes y no puedes hacer.

Qué 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 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 función o plantillas de función miembro que acepten / devuelven punteros / referencias al tipo incompleto (pero sin utilizar 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 aceptan / devuelven punteros / referencias a una de sus instancias incompletas (pero sin utilizar 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 que utilizan 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 una 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 usando una de sus instancias

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
    
  • Utiliza los métodos o campos de una de sus instancias, de hecho, intenta desreferenciar 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>;
    

9
2017-10-31 05:00



En el archivo en el que solo utiliza el puntero o la referencia a una clase. Y no se debe invocar ninguna función miembro / miembro pensada en esos puntero / referencia.

con class Foo;// declaración 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 la clase.


5
2018-02-16 15:40



Siempre que no necesite la definición (punteros de pensar y referencias) puede salirse con declaraciones directas. Esta es la razón por la cual, en su mayoría, los vería en encabezados, mientras que los archivos de implementación generalmente extraerán el encabezado para la (s) definición (es) apropiada (s).


3
2018-02-16 15:34



La regla general que sigo no es incluir ningún archivo de encabezado a menos que sea necesario. 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 forward.


2
2018-02-16 16:28



Escribo esto como una respuesta separada en lugar de solo un comentario porque estoy en desacuerdo con la respuesta de Luc Touraille, no sobre la base de la legalidad, sino de 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, solo está diciendo que pueden pasar por un puntero o una referencia que a su vez solo han conocido a través de una declaración directa.

Cuando devuelve un tipo incompleto X f2(); entonces dices que tu interlocutor debetienen la especificación de tipo completo de X. Lo necesitan para crear el LHS u objeto temporal 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 se devolvió 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 usan las funciones que declara.

Excepto

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

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


2
2017-07-06 02:54