Pourquoi l'optimisation de base vide est-elle interdite lorsque la classe de base vide est également une variable membre?

14

L'optimisation de la base vide est excellente. Cependant, il est livré avec la restriction suivante:

L'optimisation de base vide est interdite si l'une des classes de base vide est également le type ou la base du type du premier membre de données non statique, car les deux sous-objets de base du même type doivent avoir des adresses différentes dans la représentation d'objet du type le plus dérivé.

Pour expliquer cette restriction, considérez le code suivant. Le static_assertéchouera. Alors que la modification de Fooou Barde hériter à la place Base2évitera l'erreur:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

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

Je comprends parfaitement ce comportement. Ce que je ne comprends pas, c'est pourquoi ce comportement particulier existe . Il a évidemment été ajouté pour une raison, car il s'agit d'un ajout explicite et non d'un oubli. Quelle en est la raison?

En particulier, pourquoi les deux sous-objets de base devraient-ils avoir des adresses différentes? Dans ce qui précède, Barest un type et fooest une variable membre de ce type. Je ne vois pas pourquoi la classe de base Barimporte pour la classe de base du type de foo, ou vice-versa.

En effet, je ne sais quoi, je m'attendrais à ce que ce &foosoit la même que l'adresse de l' Barinstance qui la contient - comme cela doit être dans d'autres situations (1) . Après tout, je ne fais rien d'extraordinaire avec l' virtualhéritage, les classes de base sont vides malgré tout, et la compilation avec Base2montre que rien ne casse dans ce cas particulier.

Mais il est clair que ce raisonnement est incorrect d'une manière ou d'une autre, et il existe d'autres situations où cette limitation serait requise.

Disons que les réponses devraient être pour C ++ 11 ou plus récent (j'utilise actuellement C ++ 17).

(1) Remarque: EBO a été mis à niveau en C ++ 11, et en particulier est devenu obligatoire pour StandardLayoutTypes (bien que Bar, ci-dessus, ce ne soit pas a StandardLayoutType).

imallett
la source
4
Comment la justification que vous avez citée (" puisque les deux sous-objets de base du même type doivent avoir des adresses différentes ") est-elle insuffisante? Différents objets du même type doivent avoir des adresses distinctes, et cette exigence garantit que nous ne violons pas cette règle. Si une optimisation de base vide est appliquée ici, nous pourrions avoir Base *a = new Bar(); Base *b = a->foo;avec a==b, mais aet ce bsont clairement des objets différents (peut-être avec des remplacements de méthode virtuelle différents).
Toby Speight
1
La réponse de la langue-avocat cite les parties pertinentes de la spécification. Et il semble que vous le sachiez déjà.
Déduplicateur
3
Je ne suis pas sûr de comprendre quel type de réponse vous cherchez ici. Le modèle objet C ++ est ce qu'il est. La restriction est là car le modèle d'objet l'exige. Que cherchez-vous de plus au-delà de cela?
Nicol Bolas
@TobySpeight Différents objets du même type sont requis pour avoir des adresses distinctes Il est facilement possible de casser cette règle dans un programme avec un comportement bien défini.
Lawyer
@TobySpeight Non, je ne veux pas dire que vous avez oublié de dire à propos de la durée de vie: "Un objet différent du même type avec sa durée de vie " . Il est possible d'avoir plusieurs objets du même type, tous vivants, à la même adresse. Il y a au moins 2 bogues dans le libellé permettant cela.
Law Lawyer

Réponses:

4

Ok, il semble que je me sois trompé tout le temps, car pour tous mes exemples, il doit exister une table virtuelle pour l'objet de base, ce qui empêcherait au départ l'optimisation de base vide. Je vais laisser les exemples tenir car je pense qu'ils donnent des exemples intéressants de la raison pour laquelle les adresses uniques sont normalement une bonne chose à avoir.

Après avoir étudié tout cela plus en profondeur, il n'y a aucune raison technique de désactiver l'optimisation de classe de base vide lorsque le premier membre est du même type que la classe de base vide. C'est juste une propriété du modèle objet C ++ actuel.

Mais avec C ++ 20, il y aura un nouvel attribut [[no_unique_address]]qui indique au compilateur qu'un membre de données non statique peut ne pas avoir besoin d'une adresse unique (techniquement parlant, elle chevauche potentiellement [intro.object] / 7 ).

Cela implique que (c'est moi qui souligne)

Le membre de données non statique peut partager l'adresse d'un autre membre de données non statique ou celle d'une classe de base , [...]

par conséquent, on peut "réactiver" l'optimisation de classe de base vide en donnant l'attribut au premier membre de données [[no_unique_address]]. J'ai ajouté un exemple ici qui montre comment cela (et tous les autres cas auxquels je pourrais penser) fonctionne.

Mauvais exemples de problèmes à travers ce

Puisqu'il semble qu'une classe vide peut ne pas avoir de méthodes virtuelles, permettez-moi d'ajouter un troisième exemple:

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

Mais les deux derniers appels sont les mêmes.

Anciens exemples (ne répondent probablement pas à la question car les classes vides peuvent ne pas contenir de méthodes virtuelles, semble-t-il)

Considérez dans votre code ci-dessus (avec des destructeurs virtuels ajoutés) l'exemple suivant

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.

Mais comment le compilateur doit-il distinguer ces deux cas?

Et peut-être un peu moins artificiel:

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

Mais les deux derniers sont les mêmes si nous avons une optimisation de classe de base vide!

n314159
la source
1
On pourrait dire que le deuxième appel a de toute façon un comportement indéfini. Le compilateur n'a donc rien à distinguer.
StoryTeller - Unslander Monica
1
Une classe avec des membres virtuels n'est pas vide, donc sans importance ici!
Déduplicateur
1
@Deduplicator Avez-vous un devis standard à ce sujet? Cppref nous dit qu'une classe vide est "une classe ou une structure qui n'a pas de membres de données non statiques".
n314159
1
@ n314159 std::is_emptysur cppreference est beaucoup plus élaboré. Même du projet en cours sur eel.is .
Déduplicateur
2
Vous ne pouvez pas dynamic_castquand ce n'est pas polymorphe (avec des exceptions mineures non pertinentes ici).
TC