Uno de los problemas de pimpl es la penalización de rendimiento de usarlo (asignación de memoria adicional, miembros de datos no contiguos, indirecciones adicionales, etc.). Me gustaría proponer una variación en el lenguaje de pimpl que evitará estas penalizaciones de rendimiento a expensas de no obtener todos los beneficios de pimpl. La idea es dejar a todos los miembros de datos privados en la clase misma y mover solo los métodos privados a la clase pimpl. El beneficio en comparación con el pimpl básico es que la memoria permanece contigua (sin indirección adicional). Los beneficios en comparación con no usar pimpl son:
- Oculta las funciones privadas.
- Puede estructurarlo para que todas estas funciones tengan un enlace interno y permitan que el compilador lo optimice de manera más agresiva.
Entonces, mi idea es hacer que el pimpl herede de la clase en sí (suena un poco loco, lo sé, pero tengan paciencia conmigo). Se vería algo así:
En el archivo Ah:
class A
{
A();
void DoSomething();
protected: //All private stuff have to be protected now
int mData1;
int mData2;
//Not even a mention of a PImpl in the header file :)
};
En el archivo A.cpp:
#define PCALL (static_cast<PImpl*>(this))
namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
static_assert(sizeof(PImpl) == sizeof(A),
"Adding data members to PImpl - not allowed!");
void DoSomething1();
void DoSomething2();
//No data members, just functions!
};
void PImpl::DoSomething1()
{
mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
DoSomething2();
}
void PImpl::DoSomething2()
{
mData2 = baz();
}
}
A::A(){}
void A::DoSomething()
{
mData2 = foo();
PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}
Hasta donde veo, no hay absolutamente ninguna penalización de rendimiento al usar este vs no pimpl y algunas posibles ganancias de rendimiento y una interfaz de archivo de encabezado más limpia. Una desventaja que esto tiene frente al pimpl estándar es que no puede ocultar los miembros de datos, por lo que los cambios en esos miembros de datos aún desencadenarán una recopilación de todo lo que depende del archivo de encabezado. Pero a mi modo de ver, es obtener ese beneficio o el beneficio de rendimiento de tener a los miembros contiguos en la memoria (o hacer este truco- "Por qué el intento n. ° 3 es deplorable"). Otra advertencia es que si A es una clase con plantilla, la sintaxis se vuelve molesta (ya sabes, no puedes usar mData1 directamente, debes hacer esto-> mData1, y debes comenzar a usar el nombre de tipo y quizás las palabras clave de plantilla para los tipos dependientes y tipos con plantilla, etc.). Sin embargo, otra advertencia es que ya no puede usar private en la clase original, solo miembros protegidos, por lo que no puede restringir el acceso desde ninguna clase heredada, no solo el pimpl. Lo intenté pero no pude solucionar este problema. Por ejemplo, intenté convertir el pimpl en una clase de plantilla de amigo con la esperanza de que la declaración de amigo sea lo suficientemente amplia como para permitirme definir la clase de pimpl real en un espacio de nombres anónimo, pero eso simplemente no funciona. Si alguien tiene alguna idea de cómo mantener la privacidad de los miembros de datos y aún permitir que una clase de pimpl heredada definida en un espacio de nombres anónimo acceda a ellos, ¡realmente me gustaría verlo! Eso eliminaría mi reserva principal de usar esto.
Sin embargo, creo que estas advertencias son aceptables para los beneficios de lo que propongo.
Intenté buscar en línea alguna referencia a este modismo de "pimpl solo de función" pero no pude encontrar nada. Estoy realmente interesado en lo que la gente piensa sobre esto. ¿Hay otros problemas con esto o razones por las que no debería usar esto?
ACTUALIZAR:
He encontrado esta propuesta que más o menos trata de lograr exactamente lo que soy, pero lo hace cambiando el estándar. Estoy completamente de acuerdo con esa propuesta y espero que se convierta en el estándar (no sé nada de ese proceso, así que no tengo idea de la probabilidad de que eso suceda). Prefiero tener esto posible a través de un mecanismo de lenguaje incorporado. La propuesta también explica los beneficios de lo que estoy tratando de lograr mucho mejor que yo. Tampoco tiene el problema de romper la encapsulación como lo ha hecho mi sugerencia (privado -> protegido). Aún así, hasta que esa propuesta se convierta en el estándar (si eso sucede), creo que mi sugerencia hace posible obtener esos beneficios, con las advertencias que mencioné.
ACTUALIZACIÓN2:
Una de las respuestas menciona LTO como una posible alternativa para obtener algunos de los beneficios (optimizaciones más agresivas, supongo). No estoy realmente seguro de qué sucede exactamente en varios pases de optimización del compilador, pero tengo un poco de experiencia con el código resultante (uso gcc). Simplemente poner los métodos privados en la clase original obligará a aquellos a tener un enlace externo.
Podría estar equivocado aquí, pero la forma en que lo interpreto es que el optimizador de tiempo de compilación no puede eliminar la función incluso si todas sus instancias de llamada están completamente integradas dentro de esa TU. Por alguna razón, incluso LTO se niega a deshacerse de la definición de la función, incluso si parece que todas las instancias de llamada en todo el binario vinculado están en línea. Encontré algunas referencias que indican que es porque el vinculador no sabe si de alguna manera llamarás a la función usando punteros de función (aunque no entiendo por qué el vinculador no puede darse cuenta de que la dirección de ese método nunca se toma )
Este no es el caso si utiliza mi sugerencia y coloca esos métodos privados en un pimpl dentro de un espacio de nombres anónimo. Si se alinean, las funciones NO aparecerán en (con -O3, que incluye -finline-functions) el archivo de objeto.
Según tengo entendido, el optimizador, al decidir si se debe incorporar o no una función, tiene en cuenta su impacto en el tamaño del código. Entonces, usando mi sugerencia, estoy haciendo un poco "más barato" para que el optimizador incorpore esos métodos privados.
PCALL
es un comportamiento indefinido. No puede convertir unA
aPImpl
y usarlo a menos que el objeto subyacente sea realmente de tipoPImpl
. Sin embargo, a menos que me equivoque, los usuarios solo crearán objetos de tipoA
.