C++/compilación: es posible establecer el tamaño del vptr(vtable global+índice de 2 bytes)
compilation g++ (1)
Desafortunadamente ... no automáticamente.
Pero recuerde que una tabla v no es más que azúcar sintáctico para el polimorfismo en tiempo de ejecución. Si está dispuesto a rediseñar su código, hay varias alternativas.
- Polimorfismo externo
- V-tablas hechas a mano
- Polimorfismo artesanal.
1) Polimorfismo externo
La idea es que a veces solo se necesita polimorfismo de forma transitoria. Es decir, por ejemplo:
std::vector<Cat> cats;
std::vector<Dog> dogs;
std::vector<Ostrich> ostriches;
void dosomething(Animal const& a);
Parece un desperdicio para Cat
o Dog
tener un puntero virtual incrustado en esta situación porque conoce el tipo dinámico (se almacenan por valor).
El polimorfismo externo se trata de tener tipos de concreto puro e interfaces puras, así como un simple puente en el medio para adaptar temporalmente (o permanentemente, pero no es lo que se quiere aquí) a un interfaz para una interfaz.
// Interface
class Animal {
public:
virtual ~Animal() {}
virtual size_t age() const = 0;
virtual size_t weight() const = 0;
virtual void eat(Food const&) = 0;
virtual void sleep() = 0;
private:
Animal(Animal const&) = delete;
Animal& operator=(Animal const&) = delete;
};
// Concrete class
class Cat {
public:
size_t age() const;
size_t weight() const;
void eat(Food const&);
void sleep(Duration);
};
El puente está escrito de una vez por todas:
template <typename T>
class AnimalT: public Animal {
public:
AnimalT(T& r): _ref(r) {}
virtual size_t age() const override { return _ref.age(); }
virtual size_t weight() const { return _ref.weight(); }
virtual void eat(Food const& f) override { _ref.eat(f); }
virtual void sleep(Duration const d) override { _ref.sleep(d); }
private:
T& _ref;
};
template <typename T>
AnimalT<T> iface_animal(T& r) { return AnimalT<T>(r); }
Y puedes usarlo así:
for (auto const& c: cats) { dosomething(iface_animal(c)); }
Incurre en una sobrecarga de dos punteros por elemento, pero solo mientras necesites polimorfismo.
Una alternativa es hacer que AnimalT<T>
trabaje con valores también (en lugar de referencias) y que proporcione un método de clone
, lo que le permite elegir entre tener un v-puntero o no dependiendo de la situación.
En este caso, te aconsejo usar una clase simple:
template <typename T> struct ref { ref(T& t): _ref(t); T& _ref; };
template <typename T>
T& deref(T& r) { return r; }
template <typename T>
T& deref(ref<T> const& r) { return r._ref; }
Y luego modificar un poco el puente:
template <typename T>
class AnimalT: public Animal {
public:
AnimalT(T r): _r(r) {}
std::unique_ptr< Animal<T> > clone() const { return { new Animal<T>(_r); } }
virtual size_t age() const override { return deref(_r).age(); }
virtual size_t weight() const { return deref(_r).weight(); }
virtual void eat(Food const& f) override { deref(_r).eat(f); }
virtual void sleep(Duration const d) override { deref(_r).sleep(d); }
private:
T _r;
};
template <typename T>
AnimalT<T> iface_animal(T r) { return AnimalT<T>(r); }
template <typename T>
AnimalT<ref<T>> iface_animal_ref(T& r) { return Animal<ref<T>>(r); }
De esta manera usted elige cuando quiere almacenamiento polimórfico y cuándo no.
2) V-tablas hechas a mano
(solo funciona facilmente en jerarquias cerradas)
Es común en C emular la orientación a objetos al proporcionar su propio mecanismo de v-table. Ya que parece saber qué es una v-table y cómo funciona el v-puntero, entonces usted mismo puede trabajar perfectamente.
struct FooVTable {
typedef void (Foo::*DoFunc)(int, int);
DoFunc _do;
};
Y luego proporcione una matriz global para la jerarquía anclada en Foo
:
extern FooVTable const* const FooVTableFoo;
extern FooVTable const* const FooVTableBar;
FooVTable const* const FooVTables[] = { FooVTableFoo, FooVTableBar };
enum class FooVTableIndex: unsigned short {
Foo,
Bar
};
Entonces todo lo que necesitas en tu clase de Foo
es mantener el tipo más derivado:
class Foo {
public:
void dofunc(int i, int j) {
(this->*(table()->_do))(i, j);
}
protected:
FooVTable const* table() const { return FooVTables[_vindex]; }
private:
FooVTableIndex _vindex;
};
La jerarquía cerrada está ahí debido a la matriz FooVTables
y la enumeración FooVTableIndex
que deben conocer todos los tipos de jerarquía.
Sin embargo, el índice de enumeración se puede omitir y, al hacer que la matriz no sea constante, es posible preinicializar a un tamaño más grande y luego, al inicio, cada tipo derivado se registra allí automáticamente. Por lo tanto, los conflictos de índices se detectan durante esta fase inicial, e incluso es posible tener una resolución automática (escaneando la matriz en busca de una ranura libre).
Esto puede ser menos conveniente, pero proporciona una manera de abrir la jerarquía. Obviamente, es más fácil codificar antes de lanzar cualquier hilo, ya que estamos hablando de variables globales aquí.
3) Polimorfismo hecho a mano.
(solo funciona realmente para jerarquias cerradas)
El último se basa en mi experiencia explorando el código base de LLVM / Clang. Un compilador tiene el mismo problema con el que se enfrenta: para decenas o cientos de miles de elementos pequeños, un vpointer por elemento realmente aumenta el consumo de memoria, lo que es molesto.
Por lo tanto, tomaron un enfoque simple:
- Cada jerarquía de clases tiene una
enum
enumera a todos los miembros. - cada clase en la jerarquía pasa su
enumerator
compañero a su base en la construcción - La virtualidad se logra cambiando la
enum
y lanzando apropiadamente
En codigo:
enum class FooType { Foo, Bar, Bor };
class Foo {
public:
int dodispatcher() {
switch(_type) {
case FooType::Foo:
return static_cast<Foo&>(*this).dosomething();
case FooType::Bar:
return static_cast<Bar&>(*this).dosomething();
case FooType::Bor:
return static_cast<Bor&>(*this).dosomething();
}
assert(0 && "Should never get there");
}
private:
FooType _type;
};
Los interruptores son bastante molestos, pero pueden ser más o menos automatizados al jugar con algunas macros y listas de tipos. LLVM normalmente usa un archivo como:
// FooList.inc
ACT_ON(Foo)
ACT_ON(Bar)
ACT_ON(Bor)
y luego haces
void Foo::dodispatcher() {
switch(_type) {
# define ACT_ON(X) case FooType::X: return static_cast<X&>(*this).dosomething();
# include "FooList.inc"
# undef ACT_ON
}
assert(0 && "Should never get there");
}
Chris Lattner comentó que debido a la forma en que se generan los conmutadores (utilizando una tabla de compensaciones de código), este código produjo un código similar al de un envío virtual y, por lo tanto, tuvo aproximadamente la misma cantidad de sobrecarga de CPU, pero para una menor sobrecarga de memoria.
Obviamente, el único inconveniente es que Foo.cpp
debe incluir todos los encabezados de sus clases derivadas. Que efectivamente sella la jerarquía.
Presenté voluntariamente las soluciones desde la más abierta hasta la más cerrada. Tienen diversos grados de complejidad / flexibilidad, y depende de usted elegir el que más le convenga.
Una cosa importante, en los dos últimos casos, la destrucción y las copias requieren un cuidado especial.
Recientemente publiqué una pregunta sobre la sobrecarga de memoria debida a la virtualidad en C ++. Las respuestas me permiten entender cómo funcionan vtable y vptr. Mi problema es el siguiente: trabajo en supercomputadoras, tengo miles de millones de objetos y, por lo tanto, tengo que preocuparme por la sobrecarga de memoria debida a la virtualidad. Después de algunas medidas, cuando uso clases con funciones virtuales, cada objeto derivado tiene su vptr de 8 bytes. Esto no es despreciable en absoluto.
Me pregunto si intel icpc o g ++ tienen alguna configuración / opción / parámetros, para usar vtables e índices "globales" con precisión ajustable en lugar de vptr. Porque tal cosa me permitiría usar un índice de 2 bytes (int corto sin signo) en lugar de vptr de 8 bytes para miles de millones de objetos (y una buena reducción de la sobrecarga de memoria). ¿Hay alguna manera de hacer eso (o algo así) con opciones de compilación?
Muchas gracias.