Perché l'ottimizzazione della base vuota è vietata quando anche la classe base vuota è una variabile membro?


14

L'ottimizzazione della base vuota è ottima. Tuttavia, viene fornito con la seguente limitazione:

L'ottimizzazione della base vuota è vietata se una delle classi di base vuote è anche il tipo o la base del tipo del primo membro di dati non statico, poiché i due oggetti secondari di base dello stesso tipo devono avere indirizzi diversi all'interno della rappresentazione dell'oggetto del tipo più derivato.

Per spiegare questa limitazione, considerare il seguente codice. La static_assertvolontà fallirà. Considerando che la modifica di Fooo Barda cui ereditare invece Base2eviterà l'errore:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

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

Capisco questo comportamento completamente. Quello che non capisco è perché esiste questo comportamento particolare . È stato ovviamente aggiunto per una ragione, dal momento che è un'aggiunta esplicita, non una svista. Qual è una logica per questo?

In particolare, perché i due oggetti secondari di base dovrebbero avere indirizzi diversi? In quanto sopra, Barè un tipo ed fooè una variabile membro di quel tipo. Non vedo il motivo per cui la classe base delle Barcose contenga la classe base del tipo di foo, o viceversa.

Anzi, se non altro, mi aspetterei che &foosia uguale all'indirizzo Bardell'istanza che lo contiene, poiché è necessario che si trovi in ​​altre situazioni (1) . Dopotutto, non sto facendo nulla di stravagante con l' virtualereditarietà, le classi di base sono vuote a prescindere e la compilazione con Base2mostra che nulla si rompe in questo caso particolare.

Ma chiaramente questo ragionamento non è corretto in qualche modo, e ci sono altre situazioni in cui questa limitazione sarebbe richiesta.

Diciamo che le risposte dovrebbero essere per C ++ 11 o più recenti (attualmente sto usando C ++ 17).

(1) Nota: EBO è stato aggiornato in C ++ 11 e, in particolare, è diventato obbligatorio per StandardLayoutTypes (sebbene Bar, sopra, non sia un StandardLayoutType).


4
In che modo la logica che hai citato (" poiché i due oggetti secondari dello stesso tipo devono avere indirizzi diversi ") non è valida? Oggetti diversi dello stesso tipo devono avere indirizzi distinti e questo requisito garantisce che non infrangiamo quella regola. Se l'ottimizzazione della base vuota viene applicata qui, potremmo avere Base *a = new Bar(); Base *b = a->foo;con a==b, ma ae bsono chiaramente oggetti diversi (forse con diverse sostituzioni di metodi virtuali).
Toby Speight,

1
La risposta dell'avvocato linguista sta citando le parti pertinenti della specifica. E sembra che tu lo sappia già.
Deduplicatore

3
Non sono sicuro di capire che tipo di risposta stai cercando qui. Il modello a oggetti C ++ è quello che è. La restrizione esiste perché il modello a oggetti lo richiede. Cosa cerchi di più?
Nicol Bolas

@TobySpeight Sono richiesti oggetti diversi dello stesso tipo per avere indirizzi distinti È possibile infrangere questa regola in un programma con un comportamento ben definito.
Avvocato linguistico

@TobySpeight No, non intendo che ti sei dimenticato di dire della vita: "Oggetto diverso dello stesso tipo durante la vita " . È possibile avere più oggetti dello stesso tipo, tutti vivi, allo stesso indirizzo. Ci sono almeno 2 bug nel testo che lo consente.
Avvocato linguistico

Risposte:


4

Ok, mi sembra di aver sbagliato tutto il tempo, dato che per tutti i miei esempi è necessario che esista una vtable per l'oggetto base, che impedirebbe l'ottimizzazione della base vuota. Lascerò che gli esempi rimangano poiché penso che forniscano alcuni esempi interessanti del perché gli indirizzi univoci sono normalmente una buona cosa da avere.

Avendo studiato tutto questo in modo più approfondito, non esiste alcun motivo tecnico per disabilitare l'ottimizzazione della classe base vuota quando il primo membro è dello stesso tipo della classe base vuota. Questa è solo una proprietà dell'attuale modello di oggetti C ++.

Ma con C ++ 20 ci sarà un nuovo attributo [[no_unique_address]]che dice al compilatore che un membro di dati non statico potrebbe non aver bisogno di un indirizzo univoco (tecnicamente parlando è potenzialmente sovrapposto [intro.object] / 7 ).

Ciò implica che (enfatizzare il mio)

Il membro di dati non statico può condividere l'indirizzo di un altro membro di dati non statico o quello di una classe base , [...]

quindi si può "riattivare" l'ottimizzazione della classe base vuota dando l'attributo al primo membro di dati [[no_unique_address]]. Ho aggiunto un esempio qui che mostra come funziona questo (e tutti gli altri casi a cui potrei pensare).

Esempi sbagliati di problemi attraverso questo

Dato che una classe vuota potrebbe non avere metodi virtuali, vorrei aggiungere un terzo esempio:

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

Ma le ultime due chiamate sono uguali.

Vecchi esempi (probabilmente non rispondono alla domanda poiché le classi vuote potrebbero non contenere metodi virtuali, a quanto pare)

Considera nel tuo codice sopra (con l'aggiunta di distruttori virtuali) il seguente esempio

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.

Ma come dovrebbe il compilatore distinguere questi due casi?

E forse un po 'meno elaborato:

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

Ma gli ultimi due sono gli stessi se abbiamo l'ottimizzazione della classe base vuota!


1
Si potrebbe sostenere che la seconda chiamata ha comunque un comportamento indefinito. Quindi il compilatore non deve distinguere nulla.
StoryTeller - Unslander Monica

1
Una classe con qualsiasi membro virtuale non è vuota, quindi irrilevante qui!
Deduplicatore

1
@Deduplicator Hai un preventivo standard su questo? Cppref ci dice che una classe vuota è "una classe o struttura che non ha membri di dati non statici".
n314159

1
@ n314159 std::is_emptysu cppreference è molto più elaborato. Stesso dal progetto corrente su eel.is .
Deduplicatore

2
Non è possibile dynamic_castquando non è polimorfico (con eccezioni minori non rilevanti qui).
TC
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.