c++ - transitivos - #Incluir directo explícito vs.#incluido transitivo no contractual
verbos transitivos e intransitivos pdf (6)
para evitar una dependencia en el funcionamiento interno de MyClass. ¿O debería?
Sí, deberías y por mucho por esa razón.
A menos que desee especificar que MyClass.hpp está garantizado para incluir
<vector>
, no puede confiar en uno, incluido el otro.
Y no hay ninguna buena razón para ser forzado a proporcionar dicha garantía.
Si no existe tal garantía, entonces confía en un detalle de implementación de MyClass.hpp que puede cambiar en el futuro, lo que romperá su código.
Obviamente me doy cuenta de que MyClass necesita vectores para trabajar.
¿Lo hace?
¿No podría usar, por ejemplo,
boost::container::small_vector
?
En este ejemplo, MyClass necesita std :: vector
Pero ¿qué pasa con las necesidades de MyClass en el futuro? Los programas evolucionan, y lo que una clase necesita hoy no es siempre lo mismo que la clase necesita mañana.
Pero, ¿no sería bueno poder decidir qué encabezados quedan expuestos al importar?
La prevención de la inclusión transitiva no es posible.
Los módulos introducidos en C ++ 20 son una característica que se puede usar en lugar de pp-inclusión y están pensados para ayudar a resolver esto.
En este momento, puede evitar incluir dependencias de detalles de implementación utilizando el patrón PIMPL ("Puntero a implementación"). Pero PIMPL introduce una capa de direccionamiento indirecto y, lo que es más importante, requiere una asignación dinámica que tenga implicaciones de rendimiento. Dependiendo del contexto, estas implicaciones pueden ser insignificantes o significativas.
Digamos que tenemos este archivo de encabezado:
MyClass.hpp
#pragma once
#include <vector>
class MyClass
{
public:
MyClass(double);
/* ... */
private:
std::vector<double> internal_values;
};
Ahora, cuando usamos
#include "MyClass.hpp"
en algún otro archivo hpp o cpp, efectivamente también
#include <vector>
, a pesar del hecho de que no lo necesitamos.
La razón por la que digo que no es necesario es que
std::vector
solo se usa internamente en
MyClass
, pero no es necesario en absoluto
para interactuar con esta clase
.
Como resultado, pude escribir
Versión 1: SomeOtherHeader.hpp
#pragma once
#include "MyClass.hpp"
void func(const MyClass&, const std::vector<double>&);
mientras que probablemente debería escribir
Versión 2: SomeOtherHeader.hpp
#pragma once
#include "MyClass.hpp"
#include <vector>
void func(const MyClass&, const std::vector<double>&);
para evitar una dependencia en el funcionamiento interno de
MyClass
.
¿O debería?
Obviamente, me doy cuenta de que
MyClass
necesita
<vector>
para funcionar.
Así que esto puede ser más una cuestión filosófica.
Pero, ¿no sería bueno poder decidir qué encabezados se exponen al importar (es decir, limitar lo que se carga en el espacio de nombres)?
¿De modo que cada encabezado debe
#include
lo
que
necesita, sin alejarse al incluir implícitamente algo que
otro
encabezado necesitaba en la cadena?
Quizás la gente también pueda arrojar algo de luz sobre los próximos módulos de C ++ 20 que, en mi opinión, abordan algunos aspectos de este problema.
Como han dicho otros, es más seguro incluir directamente los archivos que utiliza, en términos de estar protegido de futuros cambios en el archivo en el que está confiando para reenviarlo.
En general, también se considera más limpio tener sus dependencias inmediatamente allí. Si desea verificar qué es este objeto "MyClass", solo tiene que desplazarse hasta la parte superior y pedirle a su IDE que lo lleve al encabezado correspondiente.
Vale la pena señalar que es seguro incluir el mismo encabezado estándar varias veces, como lo proporciona una garantía estándar de la biblioteca.
En la práctica, eso significa que la implementación de (
en el ejemplo, libc ++ de clang
) comenzará con una guarda
#include
.
Los compiladores modernos están tan familiarizados con la inclusión del lenguaje guardián (especialmente cuando se aplican en sus propias implementaciones de biblioteca estándar) que pueden evitar incluso cargar los archivos.
Así que lo único que pierde a cambio de esa seguridad y claridad es tener que escribir una docena de letras adicionales.
Todo lo que se acordó con todos los demás, lo he vuelto a leer y no creo que su pregunta fuera en realidad "¿Debo hacer esto?" tanto como "¿Por qué se me permite no hacer esto?" O "¿Por qué el compilador no me aísla de mis inclusiones ''incluye?"
Hay una excepción importante a la regla "incluir directamente lo que usas".
Esos son los encabezados que,
como parte de su especificación
, incluyen encabezados adicionales.
Por ejemplo, <
iostream
> (que, por supuesto, es parte de la biblioteca estándar) está garantizado en c ++ 11 para incluir
<istream>
y
<ostream>
.
Se podría decir "¿por qué no simplemente el contenido de
<istream>
y
<ostream>
movido a
<iostream>
directamente?"
pero hay ventajas de claridad y velocidad de compilación al tener la opción de dividirlas si solo se necesita una.
(Y, sin duda para c ++, también hay razones históricas) Por supuesto, también puede hacer esto para sus propios encabezados.
(Es más una cosa de Objective-C, pero tienen la misma mecánica de uso y las usan convencionalmente para encabezados de sombrilla, cuyo único trabajo es incluir otros archivos).
Hay otra razón fundamental por la que los encabezados de tu inclusión incluyen la inclusión.
Es que, en general, sus encabezados no tienen sentido sin ellos.
Supongamos que su archivo
MyClass.hpp
contiene el siguiente sinónimo de tipo
using NumberPack = std::vector<unsigned int>;
y la siguiente función auto-descriptiva
NumberPack getFirstTenNumers();
Ahora suponga que otro archivo incluye
MyClass.hpp
y tiene lo siguiente.
NumberPack counter = getFirstTenNumbers();
for (auto c : counter) {
std::cout << c << "/n"
}
Lo que sucede aquí es que es posible que no desee escribir en su código que está utilizando
<vector>
.
Ese es un detalle de implementación del que no debes tener que preocuparte.
NumberPack
podría, en lo que a usted concierne, implementarse como algún otro contenedor, un iterador, un generador o una cosa más, siempre que siga su especificación.
Pero el compilador necesita saber qué es en realidad: no puede hacer un uso efectivo de las dependencias de los padres sin saber cuáles son los encabezados de dependencia de los abuelos.
Un efecto secundario de esto es que te salgas con la suya.
O, por supuesto, la tercera razón es simplemente "porque eso no es C ++". Sí, uno podría tener un lenguaje en el que no se pasaran las dependencias de segunda generación, o tenía que solicitarlo expresamente. Es solo que sería un idioma diferente y, en particular, no encajaría en el estilo de texto antiguo incluido en c ++ o amigos.
Debe usar
#include
s explícito para tener un flujo de trabajo no destructivo.
Digamos que
MyClass
se utiliza en 50 archivos de origen diferentes.
No incluyen
vector
.
De repente, debes cambiar
std::vector
en
MyClass.h
para algún otro contenedor.
Luego, todos los 50 archivos de origen deberán incluir
vector
o deberá dejarlo en
MyClass.h
.
Esto sería redundante y podría
aumentar el tamaño de la aplicación
, el tiempo de
compilación
e incluso el
tiempo de ejecución
(inicialización de variable estática) innecesariamente.
Sí, el archivo que usa
debe
incluir
<vector>
explícitamente, ya que es una dependencia que necesita.
Sin embargo, no me preocuparía.
Si alguien refactoriza
MyClass.hpp
para eliminar la
MyClass.hpp
de
<vector>
, el compilador los dirigirá a todos los archivos que carecían de la inclusión explícita de
<vector>
, confiando en la inclusión implícita.
Por lo general, no es difícil solucionar este tipo de errores, y una vez que el código se compila nuevamente, algunos de los complementos explícitos faltantes se habrán corregido.
Al final, el compilador es mucho más eficiente para detectar perdidas que cualquier otro ser humano.
Si su
MyClass
tiene un miembro de tipo
std::vector<double>
entonces el encabezado que define
MyClass
debe
#include <vector>
.
De lo contrario, la única forma en que los usuarios de
MyClass
pueden compilar es si
#include <vector>
antes de incluir la definición de
MyClass
.
Aunque el miembro es
private
, sigue siendo parte de la clase, por lo que el compilador necesita ver una definición de tipo completa.
De lo contrario, no puede hacer cosas como compute
sizeof(MyClass)
, o instanciar cualquier objeto
MyClass
.
Si desea romper la dependencia entre su encabezado y
<vector>
hay técnicas.
Por ejemplo, el lenguaje pimpl ("puntero a implementación").
class MyClass
{
public:
MyClass(double first_value);
/* ... */
private:
void *pimpl;
};
y, en el archivo fuente que define a los miembros de la clase;
#include <vector>
#include "MyClass.hpp"
MyClass::MyClass(double first_value) : pimpl(new std::vector<double>())
{
}
(y también, presumiblemente, hacer algo con
first_value
, pero lo he omitido).
La compensación es que cada función miembro que necesita usar el vector necesita obtenerla del
pimpl
.
Por ejemplo, si desea obtener una referencia al vector asignado
void MyClass::some_member_function()
{
std::vector<double> &internal_data = *static_cast<std::vector<double> *>(pimpl);
}
El destructor de
MyClass
también deberá liberar el vector asignado dinámicamente.
Esto también limita algunas opciones para la definición de la clase.
Por ejemplo,
MyClass
no puede tener una función miembro que devuelva un
std::vector<double>
por valor (a menos que
#include <vector>
)
Tendrá que decidir si las técnicas como el lenguaje pimpl valen el esfuerzo para hacer que su clase funcione.
Personalmente, a menos que existan OTRAS razones convincentes para separar la implementación de la clase de la clase utilizando el lenguaje pimpl, simplemente aceptaría la necesidad de
#include <vector>
en su archivo de encabezado.
Tenga en cuenta que el código no se escribe solo una vez, sino que evoluciona con el tiempo.
Asumamos que escribiste el código y ahora mi tarea sería refactorizarlo.
Por alguna razón, quiero reemplazar
MyClass
con
YourClass
y digamos que tienen la misma interfaz.
Simplemente tendría que reemplazar cualquier aparición de
MyClass
con
YourClass
para llegar a esto:
/* Version 1: SomeOtherHeader.hpp */
#pragma once
#include "YourClass.hpp"
void func(const YourClass& a, const std::vector<double>& b);
Hice todo correctamente, pero aún así el código no se compilaría (porque
YourClass
no incluye
std::vector
).
En este ejemplo en particular, obtendría un mensaje de error claro y la solución sería obvia.
Sin embargo, las cosas pueden complicarse bastante rápido si tales dependencias se extienden a lo largo de varios encabezados, si hay muchas de esas dependencias y si
SomeOtherHeader.hpp
contiene más que una sola declaración.
Hay más cosas que pueden salir mal.
Por ejemplo, el autor de
MyClass
podría decidir que realmente puede eliminar la inclusión a favor de una declaración en adelante.
También entonces
SomeOtherHeader
se romperá.
Se reduce a: Si no incluye
vector
en
SomeOtherHeader
entonces hay una dependencia oculta, que es mala.
La regla de oro para evitar tales problemas es: Incluya lo que usa.