resueltos - ¿Por qué las clases de C++ sin variables miembro ocupan espacio?
funciones en c++ ejemplos (4)
Descubrí que los compiladores MSVC y GCC asignan al menos un byte por cada instancia de clase, incluso si la clase es un predicado sin variables miembro (o solo con variables miembro estáticas). El siguiente código ilustra el punto.
#include <iostream>
class A
{
public:
bool operator()(int x) const
{
return x>0;
}
};
class B
{
public:
static int v;
static bool check(int x)
{
return x>0;
}
};
int B::v = 0;
void test()
{
A a;
B b;
std::cout << "sizeof(A)=" << sizeof(A) << "/n"
<< "sizeof(a)=" << sizeof(a) << "/n"
<< "sizeof(B)=" << sizeof(B) << "/n"
<< "sizeof(b)=" << sizeof(b) << "/n";
}
int main()
{
test();
return 0;
}
Salida:
sizeof(A)=1
sizeof(a)=1
sizeof(B)=1
sizeof(b)=1
Mi pregunta es ¿por qué lo necesita el compilador? La única razón por la que se me ocurre es asegurarme de que todos los punteros var de miembros difieran para que podamos distinguir entre dos miembros de tipo A o B al compararlos con ellos. Pero el costo de esto es bastante severo cuando se trata de contenedores de pequeño tamaño. Considerando la posible alineación de datos, podemos obtener hasta 16 bytes por clase sin vars (?!). Supongamos que tenemos un contenedor personalizado que normalmente contendrá algunos valores int. Luego considere un conjunto de dichos contenedores (con aproximadamente 1000000 miembros). ¡Los gastos generales serán 16 * 1000000! Un caso típico en el que puede suceder es una clase contenedor con un predicado de comparación almacenado en una variable miembro. Además, teniendo en cuenta que una instancia de clase siempre debe ocupar algo de espacio, ¿qué tipo de sobrecarga se debe esperar al llamar a A () (valor)?
Básicamente, es una interacción entre dos requisitos:
- Dos objetos diferentes del mismo tipo deben estar en direcciones diferentes.
- En las matrices, puede que no haya ningún relleno entre los objetos.
Tenga en cuenta que la primera condición por sí sola no requiere un tamaño distinto de cero: dado
struct empty {};
struct foo { empty a, b; };
El primer requisito podría cumplirse fácilmente teniendo un tamaño cero
a
seguido de un solo byte de relleno para forzar una dirección diferente, seguida de un tamaño cero
b
.
Sin embargo, dado
empty array[2];
eso ya no funciona porque no se permitiría un relleno entre los diferentes objetos
empty[0]
y
empty[1]
.
Es necesario satisfacer una invariante del estándar C ++: cada objeto C ++ del mismo tipo debe tener una dirección única para poder identificarse.
Si los objetos no ocupaban espacio, los elementos en una matriz compartirían la misma dirección.
Todos los objetos completos deben tener una dirección única; por lo tanto, deben ocupar al menos un byte de almacenamiento, el byte en su dirección.
Un caso típico en el que puede suceder es una clase contenedor con un predicado de comparación almacenado en una variable miembro.
En este caso, puede usar la optimización de clase base vacía : un subobjeto base puede tener la misma dirección que el objeto completo del que forma parte, por lo que no puede ocupar espacio de almacenamiento. Por lo tanto, puede adjuntar el predicado a una clase como una clase base (quizás privada) en lugar de un miembro. Es un poco más complicado tratar con él que un miembro, pero debería eliminar la sobrecarga.
¿Qué tipo de sobrecarga debería esperarse al llamar a A () (valor)?
La única sobrecarga en comparación con la llamada a una función no miembro será pasar el argumento adicional. Si la función está en línea, esto debería eliminarse (como sería el caso, en general, cuando se llama a una función miembro que no accede a ninguna variable miembro).
Ya hay excelentes respuestas que responden a la pregunta principal. Me gustaría abordar las inquietudes que expresó con:
Pero el costo de esto es bastante severo cuando se trata de contenedores de pequeño tamaño. Considerando la posible alineación de datos, podemos obtener hasta 16 bytes por clase sin vars (?!). Supongamos que tenemos un contenedor personalizado que normalmente contendrá algunos valores int. Luego considere un conjunto de dichos contenedores (con aproximadamente 1000000 miembros). ¡Los gastos generales serán 16 * 1000000! Un caso típico en el que puede suceder es una clase contenedor con un predicado de comparación almacenado en una variable miembro.
Evitar el costo de mantener
A
Si todas las instancias de un contenedor dependen del tipo
A
, entonces no hay necesidad de mantener instancias de
A
en el contenedor.
La sobrecarga asociada con el tamaño distinto de cero de
A
se puede evitar simplemente creando una instancia de
A
en la pila cuando sea necesario.
No poder evitar el costo de mantener
A
Puede verse obligado a mantener un puntero a
A
en cada instancia del contenedor si se espera que
A
sea polimórfico.
Para tal contenedor, el costo de cada contenedor aumenta en el tamaño de un puntero.
Si hay variables miembro en la clase base
A
o no, no hay diferencia en el tamaño del contenedor.
Impacto del
sizeof A
En cualquier caso, el tamaño de una clase vacía no debería influir en los requisitos de almacenamiento del contenedor.