significado - C++: ¿La especialización de clase es una transformación válida para un compilador conforme?
que es un interprete en programacion (2)
Esperemos que esto no sea una pregunta demasiado especializada para StackOverflow: si es y podría migrarse a otro lugar, hágamelo saber ...
Hace muchas lunas, escribí una tesis de pregrado que propone varias técnicas de desvirtualización para C ++ y lenguajes relacionados, generalmente basada en la idea de especialización precompilada de rutas de código (algo así como plantillas) pero con controles para elegir las especializaciones correctas se eligen en tiempo de ejecución en los casos no se pueden seleccionar en tiempo de compilación (como deben ser las plantillas).
La idea (muy) básica es algo como lo siguiente ... suponga que tiene una clase C
como la siguiente:
class C : public SomeInterface
{
public:
C(Foo * f) : _f(f) { }
virtual void quack()
{
_f->bark();
}
virtual void moo()
{
quack(); // a virtual call on this because quack() might be overloaded
}
// lots more virtual functions that call virtual functions on *_f or this
private:
Foo * const _f; // technically doesn''t have to be const explicitly
// as long as it can be proven not be modified
};
Y sabías que existen subclases concretas de Foo
como FooA
, FooB
, etc., con tipos completos conocidos (sin tener necesariamente una lista exhaustiva), entonces podrías precompilar versiones especializadas de C
para algunas subclases seleccionadas de Foo
, como, por ejemplo ( note que el constructor no está incluido aquí, a propósito, ya que no se llamará):
class C_FooA final : public SomeInterface
{
public:
virtual void quack() final
{
_f->FooA::bark(); // non-polymorphic, statically bound
}
virtual void moo() final
{
C_FooA::quack(); // also static, because C_FooA is final
// _f->FooA::bark(); // or you could even do this instead
}
// more virtual functions all specialized for FooA (*_f) and C_FooA (this)
private:
FooA * const _f;
};
Y reemplaza al constructor de C
con algo como lo siguiente:
C::C(Foo * f) : _f(f)
{
if(f->vptr == vtable_of_FooA) // obviously not Standard C++
this->vptr = vtable_of_C_FooA;
else if(f->vptr == vtable_of_FooB)
this->vptr = vtable_of_C_FooB;
// otherwise leave vptr unchanged for all other values of f->vptr
}
Básicamente, el tipo dinámico del objeto que se está construyendo se cambia según el tipo dinámico de los argumentos a su constructor. (Tenga en cuenta que no puede hacer esto con las plantillas porque solo puede crear un C<Foo>
si conoce el tipo de f
en tiempo de compilación). De ahora en adelante, cualquier llamada a FooA::bark()
través de C::quack()
solo involucra una llamada virtual: o bien la llamada a C::quack()
está vinculada estáticamente a la versión no especializada que dinámicamente llama a FooA::bark()
, o la llamada a C::quack()
se reenvía dinámicamente a C_FooA::quack()
que llama estáticamente a FooA::bark()
. Además, el envío dinámico podría eliminarse por completo en algunos casos si el analizador de flujo tiene suficiente información para realizar una llamada estática a C_FooA::quack()
, lo que podría ser muy útil en un circuito cerrado si permite la inclusión. (Aunque técnicamente en ese punto, probablemente estaría bien incluso sin esta optimización ...)
(Tenga en cuenta que esta transformación es segura, aunque menos útil, incluso si _f
no es constante y está protegido en lugar de privado y C
se hereda de una unidad de traducción diferente ... la unidad de traducción que crea el vtable para la clase heredada no lo sabrá todo lo relacionado con las especializaciones y el constructor de la clase heredada solo configurará this->vptr
en su propia vtable, que no hará referencia a ninguna función especializada porque no sabrá nada sobre ellas.)
Esto puede parecer un gran esfuerzo para eliminar un nivel de direccionamiento indirecto, pero el punto es que puede hacerlo a cualquier nivel de anidamiento arbitrario (cualquier profundidad de llamadas virtuales que se siga a este patrón podría reducirse a uno) basándose solo en información local. una unidad de traducción, y hágalo de una manera que sea resistente, incluso si los nuevos tipos están definidos en otras unidades de traducción que no conoce ... puede agregar una gran cantidad de código que de otra forma no tendría si no hubiera lo hizo ingenuamente
De todos modos, independientemente de si este tipo de optimización realmente tendría suficiente bang-for-the-buck valdría la pena el esfuerzo de implementación y también la sobrecarga de espacio en el ejecutable resultante , mi pregunta es, ¿hay algo en el estándar C ++ que impida un compilador de realizar tal transformación?
Mi sensación es no, ya que el estándar no especifica en absoluto cómo se realiza el envío virtual o cómo se representan las funciones de punteros a miembros. Estoy bastante seguro de que no hay nada en el mecanismo RTTI que evite que C
y C_FooA
por el mismo tipo para todos los propósitos, incluso si tienen diferentes tablas virtuales. La única otra cosa en la que podría pensar que podría importar es una lectura atenta de la ODR, pero probablemente no.
¿Estoy pasando por alto algo? Salvo problemas ABI / vinculación, ¿serían posibles transformaciones como esta sin romper los programas C ++ conformes? (Además, si es así, ¿podría hacerse esto actualmente con los ABI de Itanium y / o MSVC? Estoy bastante seguro de que la respuesta es sí, también, pero espero que alguien pueda confirmar).
EDITAR : ¿Alguien sabe si algo como esto se implementa en algún compilador / JIT convencional para C ++, Java o C #? (Consulte la discusión y el chat vinculado en los comentarios a continuación). Soy consciente de que los JIT hacen vinculación estática / alineación de virtuales directamente en los sitios de llamadas, pero no sé si hacen algo como esto (con vtables completamente nuevos) se genera y se elige en función de una comprobación de tipo único realizada en el constructor, en lugar de en cada sitio de llamada).
¿Hay algo en Standard C ++ que impida que un compilador realice tal transformación?
No si está seguro de que el comportamiento observable no cambia, esa es la "regla de" como si ", que es la sección 1.9 estándar.
Pero esto podría hacer que la prueba de que su transformación es correcta sea bastante difícil: 12.7 / 4:
Cuando se llama a una función virtual directa o indirectamente desde un constructor (incluido el inicializador de mem o el iniciador de refuerzo o igual para un miembro de datos no estáticos) o desde un destructor, y el objeto al que se aplica la llamada es el objeto en construcción o destrucción, la función llamada es la definida en la clase del constructor o destructor o en una de sus bases, pero no es una función que la sobrescriba en una clase derivada de la clase del constructor o destructor, o que la anule en una de Las otras clases base del objeto más derivado.
Entonces, si el destructor Foo::~Foo()
llama directa o indirectamente a C::quack()
en un objeto c
, donde c._f
apunta al objeto que se está destruyendo, debe llamar a Foo::bark()
, incluso si _f
era un FooA
cuando construiste el objeto c
.
En la primera lectura, esto suena como una variación centrada en c ++ del almacenamiento en caché en línea polimórfico . Creo que V8 y JVM de Oracle lo usan, y sé que .NET sí lo hace .
Para responder a su pregunta original: no creo que haya nada en el estándar que prohíba este tipo de implementaciones. C ++ toma muy en serio la regla "tal cual"; siempre que implementes fielmente la semántica correcta, puedes hacer la implementación de la manera que más te guste. Las llamadas virtuales de c ++ no son muy complicadas, por lo que dudo que te tropieces con algún caso de ventaja (a diferencia de si, por ejemplo, trataste de hacer algo inteligente con enlace estático ).