¿Por qué está prohibida la optimización de base vacía cuando la clase base vacía también es una variable miembro?

14

La optimización de la base vacía es excelente. Sin embargo, viene con la siguiente restricción:

La optimización de base vacía está prohibida si una de las clases base vacías es también el tipo o la base del tipo del primer miembro de datos no estático, ya que los dos subobjetos base del mismo tipo deben tener direcciones diferentes dentro de la representación del objeto del tipo más derivado.

Para explicar esta restricción, considere el siguiente código. El static_assertfallará. Mientras que cambiar Fooo Barheredar de en su lugar Base2evitará el error:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

static_assert(offsetof(Bar,foo)==0,"Error!");

Entiendo este comportamiento completamente. Lo que no entiendo es por qué existe este comportamiento particular . Obviamente se agregó por una razón, ya que es una adición explícita, no un descuido. ¿Cuál es la razón de esto?

En particular, ¿por qué los dos subobjetos básicos deben tener direcciones diferentes? En lo anterior, Bares un tipo y fooes una variable miembro de ese tipo. No veo por qué la clase base de Barasuntos importa a la clase base del tipo de foo, o viceversa.

De hecho, yo, en todo caso, esperaría que &foosea ​​la misma que la dirección de la Barinstancia que lo contiene, como se requiere en otras situaciones (1) . Después de todo, no estoy haciendo nada elegante con la virtualherencia, las clases base están vacías de todos modos, y la compilación con Base2muestra que nada se rompe en este caso particular.

Pero claramente este razonamiento es incorrecto de alguna manera, y hay otras situaciones en las que se requeriría esta limitación.

Digamos que las respuestas deberían ser para C ++ 11 o más reciente (actualmente estoy usando C ++ 17).

(1) Nota: EBO se actualizó en C ++ 11, y en particular se convirtió en obligatorio para StandardLayoutTypes (aunque Bar, arriba, no es a StandardLayoutType).

imallett
fuente
44
¿Cómo se queda corto el razonamiento que usted citó (" dado que los dos subobjetos básicos del mismo tipo tienen direcciones diferentes ")? Se requieren diferentes objetos del mismo tipo para tener direcciones distintas, y este requisito asegura que no rompamos esa regla. Si la optimización de la base vacía se aplica aquí, podríamos tener Base *a = new Bar(); Base *b = a->foo;con a==b, pero ay bson objetos claramente diferentes (quizás con anulaciones de métodos virtuales diferentes).
Toby Speight
1
La respuesta del abogado del idioma es citar las partes relevantes de la especificación. Y parece que ya lo sabes.
Deduplicador
3
No estoy seguro de entender qué tipo de respuesta estás buscando aquí. El modelo de objetos C ++ es lo que es. La restricción está ahí porque el modelo de objetos lo requiere. ¿Qué más buscas más allá de eso?
Nicol Bolas
@TobySpeight Se requieren diferentes objetos del mismo tipo para tener direcciones distintas Es fácil romper esta regla en un programa con un comportamiento bien definido.
Abogado de idiomas el
@TobySpeight No, no quiero decir que olvidó decir sobre la vida útil: "Objeto diferente del mismo tipo dentro de su vida útil " . Es posible tener múltiples objetos del mismo tipo, todos vivos, en la misma dirección. Hay al menos 2 errores en la redacción que lo permiten.
Abogado de idiomas

Respuestas:

4

Ok, parece que me he equivocado todo el tiempo, ya que para todos mis ejemplos debe existir una tabla v para el objeto base, lo que evitaría la optimización de la base vacía para comenzar. Dejaré los ejemplos en pie ya que creo que dan algunos ejemplos interesantes de por qué las direcciones únicas son normalmente una buena opción.

Habiendo estudiado todo esto más a fondo, no hay ninguna razón técnica para que la optimización de la clase base vacía se deshabilite cuando el primer miembro es del mismo tipo que la clase base vacía. Esto es solo una propiedad del modelo de objetos C ++ actual.

Pero con C ++ 20 habrá un nuevo atributo [[no_unique_address]]que le indica al compilador que un miembro de datos no estático puede no necesitar una dirección única (técnicamente hablando, es posible que se superponga [intro.object] / 7 ).

Esto implica que (énfasis mío)

El miembro de datos no estático puede compartir la dirección de otro miembro de datos no estático o el de una clase base , [...]

por lo tanto, se puede "reactivar" la optimización de la clase base vacía al darle al primer miembro de datos el atributo [[no_unique_address]]. Agregué un ejemplo aquí que muestra cómo funciona esto (y todos los demás casos que se me ocurren).

Ejemplos incorrectos de problemas a través de esto

Como parece que una clase vacía puede no tener métodos virtuales, permítanme agregar un tercer ejemplo:

int stupid_method(Base *b) {
  if( dynamic_cast<Foo*>(b) ) return 0;
  if( dynamic_cast<Bar*>(b) ) return 1;
  return 2;
}

Bar b;
stupid_method(&b);  // Would expect 0
stupid_method(&b.foo); //Would expect 1

Pero las dos últimas llamadas son iguales.

Ejemplos antiguos (Probablemente no responda la pregunta ya que las clases vacías pueden no contener métodos virtuales, parece)

Considere en su código anterior (con destructores virtuales agregados) el siguiente ejemplo

void delBase(Base *b) {
    delete b;
}

Bar *b = new Bar;
delBase(b); // One would expect this to be absolutely fine.
delBase(&b->foo); // Whoaa, we shouldn't delete a member variable.

Pero, ¿cómo debería el compilador distinguir estos dos casos?

Y tal vez un poco menos artificial:

struct Base { 
  virtual void hi() { std::cout << "Hello\n";}
};

struct Foo : Base {
  void hi() override { std::cout << "Guten Tag\n";}
};

struct Bar : Base {
    Foo foo;
};

Bar b;
b.hi() // Hello
b.foo.hi() // Guten Tag
Base *a = &b;
Base *z = &b.foo;
a->hi() // Hello
z->hi() // Guten Tag

¡Pero los dos últimos son iguales si tenemos una optimización de clase base vacía!

n314159
fuente
1
Sin embargo, uno podría argumentar que la segunda llamada tiene un comportamiento indefinido. Entonces el compilador no tiene que distinguir nada.
StoryTeller - Unslander Monica
1
Una clase con miembros virtuales no está vacía, ¡así que es irrelevante aquí!
Deduplicador
1
@Deduplicator ¿Tiene una cotización estándar sobre eso? Cppref nos dice que una clase vacía es "una clase o estructura que no tiene miembros de datos no estáticos".
n314159
1
@ n314159 std::is_emptyen cppreference es mucho más elaborado. Misma desde el actual proyecto de eel.is .
Deduplicador
2
No puede dynamic_castcuando no es polimórfico (con pequeñas excepciones que no son relevantes aquí).
TC