c++ forward-declaration

c++ - ¿Cuáles son los peligros de las declaraciones a futuro?



forward-declaration (9)

Bueno, aparte de las cuestiones sobre la duplicación ...

... hay al menos un punto sensible en el Estándar.

Si llama a delete en un puntero a un tipo incompleto, obtendrá un comportamiento indefinido. En la práctica, el destructor puede no ser llamado.

Podemos ver eso en LiveWorkSpace usando el siguiente comando y muestra:

// -std=c++11 -Wall -W -pedantic -O2 #include <iostream> struct ForwardDeclared; void throw_away(ForwardDeclared* fd) { delete fd; } struct ForwardDeclared { ~ForwardDeclared() { std::cout << "Hello, World!/n"; } }; int main() { ForwardDeclared* fd = new ForwardDeclared(); throw_away(fd); }

Diagnóstico:

Compilation finished with warnings: source.cpp: In function ''void throw_away(ForwardDeclared*)'': source.cpp:6:11: warning: possible problem detected in invocation of delete operator: [enabled by default] source.cpp:5:6: warning: ''fd'' has incomplete type [enabled by default] source.cpp:3:8: warning: forward declaration of ''struct ForwardDeclared'' [enabled by default] source.cpp:6:11: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined

¿No quieres agradecer a tu compilador por advertirte?)?

Acabo de tener una entrevista. Me preguntaron qué es una "declaración hacia adelante". Entonces me preguntaron si eran peligros asociados con las declaraciones a futuro.

No pude responder a la segunda pregunta. Una búsqueda en la red no mostró ningún resultado interesante.

Entonces, ¿alguien sabe algún peligro asociado con el uso de declaraciones a plazo?


El único peligro de declarar algo hacia adelante es cuando realiza la declaración hacia adelante fuera de un encabezado o en un encabezado no compartido, y la firma de la declaración hacia adelante difiere de la firma real de lo que se declara hacia adelante. Si lo hace en la extern "C" , no habría ningún nombre para verificar la firma en el momento del enlace, por lo que puede terminar con un comportamiento indefinido cuando las firmas no coinciden.


Encontré un fragmento interesante en la Guía de estilo de Google C ++

El peligro que señalan surge de la implementación de funciones en tipos incompletos. Normalmente, este compilador arrojaría un error, pero debido a que son punteros, puede deslizarse a través de la red.

Puede ser difícil determinar si se necesita una declaración hacia adelante o un #incluir completo. Reemplazar un #include con una declaración hacia adelante puede cambiar silenciosamente el significado del código:

// b.h: struct B {}; struct D : B {}; // good_user.cc: #include "b.h" void f(B*); void f(void*); void test(D* x) { f(x); } // calls f(B*)

Si el #include se reemplazó con las referencias para B y D, test () llamaría f (void *).


La declaración hacia adelante es el síntoma de que faltan módulos en C ++ (¿se solucionarán en C ++ 17?) Y el uso de la inclusión de encabezados, si C ++ tuviera módulos, no habría ninguna necesidad de declaraciones en adelante.

Una declaración de reenvío no es menos que un "contrato", al usarla, realmente promete que proporcionará la implementación de algo (después en el mismo archivo de origen, o al vincular un binario más adelante).

La desventaja de esto es que realmente tiene que cumplir su contrato , no es un gran problema porque si no lo hace, el compilador de alguna manera se quejará antes, pero en algunos idiomas el código se ejecuta sin la necesidad de "prometer su existencia propia "(hablando de lenguajes dinámicamente tipados)


La primera forma es reordenar nuestras llamadas de función para que la adición se defina antes de main:

De esa manera, para cuando la llamada main () agregue (), ya sabrá qué es el agregado. Debido a que este es un programa tan simple, este cambio es relativamente fácil de hacer. Sin embargo, en un programa grande, sería extremadamente tedioso intentar descifrar qué funciones llamaban a qué otras funciones para que pudieran declararse en el orden correcto.


Otro peligro de las declaraciones a futuro es que facilita violar la Regla de una definición. Suponiendo que tiene una declaración de class B hacia adelante (que se supone que está en bh y b.cpp), pero dentro de a.cpp usted incluye b2.h que declara una class B diferente a bh, entonces se llega a un comportamiento no definido.


Si se pasa un puntero a tipo de clase incompleta para delete , se puede pasar por alto una sobrecarga de operator delete .

Eso es todo lo que tengo ... y para ser mordido, tendrías que hacer eso y nada más en el archivo fuente que causaría un error de compilación de "tipo incompleto".

EDITAR: Siguiendo el ejemplo de los otros muchachos, diría que la dificultad (que puede considerarse peligrosa) es garantizar que la declaración hacia adelante coincida, de hecho, con la declaración real. Para las funciones y plantillas, las listas de argumentos deben mantenerse sincronizadas.

Y debe eliminar la declaración de reenvío cuando elimine lo que declara, o se quedará alrededor y creará un espacio en el espacio de nombres. Pero incluso en tales casos, el compilador lo señalará en los mensajes de error si se interpone en el camino.

El mayor peligro es no tener una declaración hacia adelante. Una desventaja importante de las clases anidadas es que no pueden ser declaradas hacia adelante (bueno, pueden hacerlo dentro del alcance de la clase adjunta, pero eso es solo breve).


Una declaración hacia adelante no es tan peligrosa en sí misma, pero es un olor a código. Si necesita una declaración hacia adelante, significa que dos clases están estrechamente acopladas, lo que generalmente es malo. Por lo tanto, es una indicación de que su código puede necesitar refactorización.

Hay algunos casos en los que está bien tener un acoplamiento apretado, por ejemplo, los estados concretos en una implementación de patrón de estado pueden estar estrechamente acoplados. Yo consideraría esto bien. Pero en la mayoría de los otros casos, mejoraría mi diseño antes de usar una declaración hacia adelante.


Yo diría que cualquier peligro es eclipsado por las ganancias. Hay algunos sin embargo, en su mayoría relacionados con la refactorización.

  • el cambio de nombre de las clases afecta a todas las declaraciones anticipadas. Por supuesto, esto también viene con incluye, pero el error se genera en un lugar diferente, por lo que es más difícil de detectar.
  • mover clases de un namespace de namespace a otro, junto con el using directivas, puede causar estragos (errores misteriosos, difíciles de detectar y corregir); por supuesto, las directivas de using son malas para comenzar, pero ningún código es perfecto, ¿verdad? *
  • plantillas : para reenviar las plantillas (especialmente las definidas por el usuario) necesitará la firma, lo que lleva a la duplicación de código.

Considerar

template<class X = int> class Y; int main() { Y<> * y; } //actual definition of the template class Z { }; template<class X = Z> //vers 1.1, changed the default from int to Z class Y {};

La clase Z se modificó posteriormente como el argumento de la plantilla predeterminada, pero la declaración original original sigue con int .

* Recientemente me he encontrado con esto:

Original:

Definición:

//3rd party code namespace A { struct X {}; }

y declaración hacia adelante:

//my code namespace A { struct X; }

Después de la refactorización:

//3rd party code namespace B { struct X {}; } namespace A { using ::B::X; }

Esto obviamente hizo que mi código fuera inválido, pero el error no estaba en el lugar real y la solución fue, por decir lo menos, sospechoso.