Est-il autorisé à écrire une instance de Derived sur une instance de Base?

Dis le code

class Derived: public Base {....} Base* b_ptr = new( malloc(sizeof(Derived)) ) Base(1); b_ptr->f(2); Derived* d_ptr = new(b_ptr) Derived(3); b_ptr->g(4); d_ptr->f(5); 

semble être raisonnable et LSP est satisfait.

Je soupçonne que ce code est autorisé par défaut lorsque Base et Derived sont POD, et non autorisé autrement (car vtbl ptr est écrasé). La première partie de ma question est la suivante: veuillez indiquer les conditions préalables précises d’un tel écrasement.

Il peut exister d’autres moyens autorisés d’écraser.

La deuxième partie de ma question est la suivante: existe-t-il d’autres moyens? Quelles sont leurs conditions préalables précises?

UPDATE: Je ne veux PAS écrire du code comme celui-ci; Je suis intéressé par la possibilité théorique (ou l’impossibilité) d’un tel code. Donc, c’est une question “nazie standard”, pas une question “comment puis-je …”. (Ma question doit-elle être déplacée vers un autre site stackoverflow?)

UPDATE2 & 4: Qu’en est-il des destructeurs? La sémantique supposée de ce code est “L’instance de base est mise à jour (de manière destructive) par une tranche d’instance dérivée”. Supposons, pour des raisons de simplicité, que la classe Base a un destructeur sortingvial.

UPDATE3: Le plus intéressant pour moi est la validité de l’access via b_ptr->g(4)

Vous devez vraiment faire b_ptr = d_ptr après le placement-nouveau de Derived , au cas où le sous-object Base ne serait pas le premier dans la présentation de Derived . Comme écrit, b_ptr->g(4) évoque un comportement indéfini.

La règle (3.8 basic.life ):

Si, après la fin de la vie d’un object et avant que le stockage dans lequel l’object occupé est réutilisé ou libéré, un nouvel object est créé à l’emplacement de stockage occupé par l’object d’origine, un pointeur pointant sur l’object d’origine , une référence qui référé à l’object d’origine, ou le nom de l’object d’origine fera automatiquement référence au nouvel object et, une fois que la durée de vie du nouvel object aura commencé, pourra être utilisé pour manipuler le nouvel object si :

  • le stockage du nouvel object recouvre exactement l’emplacement de stockage occupé par l’object d’origine, et
  • le nouvel object est du même type que l’object d’origine (en ignorant les quali fi cateurs cv de niveau supérieur), et
  • le type de l’object d’origine n’est pas qualifié de type const et, s’il s’agit d’un type de classe, il ne contient aucun membre de données non statique dont le type est qualifié de type const ou de type référence, et
  • l’object d’origine était un object le plus dérivé (1.8) de type T et le nouvel object est un object le plus dérivé de type T (c’est-à-dire qu’ils ne sont pas des sous-objects de classe de base ).

Vous devriez également probablement détruire l’ancien object avant de réutiliser sa mémoire, mais la norme ne l’exige pas. Cependant, tout manquement à cette règle entraînera une fuite des ressources appartenant à l’ancien object. La règle complète est donnée dans la section 3.8 ( basic.life ) de la norme:

Un programme peut mettre fin à la durée de vie d’un object en réutilisant le stockage occupé par l’object ou en appelant explicitement le destructeur pour un object de type classe avec un destructeur non sortingvial. Pour un object d’un type de classe avec un destructeur non sortingvial, le programme n’est pas obligé d’appeler explicitement le destructeur avant que le stockage occupé par l’object soit réutilisé ou libéré; cependant, s’il n’y a pas d’appel explicite au destructeur ou si une expression de suppression (5.3.5) n’est pas utilisée pour libérer le stockage, le destructeur ne doit pas être appelé implicitement et aucun programme dépendant des effets secondaires produits par le destructeur a un comportement indéfini .

Vous initialisez deux fois le même morceau de mémoire. Cela ne finira pas bien.

Supposons par exemple que le constructeur de la Base alloue de la mémoire et la stocke dans un pointeur. La deuxième fois via le constructeur, le premier pointeur sera écrasé et la mémoire sera perdue.

Je pense que l’écrasement est autorisé.

Si c’était moi, j’appellerais probablement Base::~Base avant de réutiliser le stockage, de sorte que la durée de vie de l’object d’origine se termine proprement. Mais la norme vous permet explicitement de réutiliser le stockage sans appeler le destructeur.

Je ne crois pas que votre access via b_ptr soit valide. La durée de vie de l’object Base est terminée.

(Voir 3.8 / 4 dans l’une ou l’autre norme pour les règles de durée de vie.)

Et je ne suis pas non plus convaincu que b_ptr doit donner la même adresse que celle renvoyée à l’origine par l’appel malloc ().

Si vous écrivez ce code plus proprement, il est plus facile de voir ce qui ne va pas:

 void * addr = std::malloc(LARGE_NUMBER); Base * b = new (addr) Base; b->foo(); // no problem Derived * d = new (addr) Derived; d->bar(); // also fine (#1) b->foo(); // Error! b no longer points to a Base! static_cast(d)->foo(); // OK b = d; b->foo(); // also OK 

Le problème est que, sur la ligne marquée (n ° 1), b et d indiquent des choses totalement distinctes et sans rapport, et que vous avez écrasé la mémoire de l’ancien object *b , b n’est en fait plus valide du tout.

Vous avez peut-être des idées erronées sur le fait que Base* et Derived* soient des types de pointeur convertibles, mais cela n’a rien à voir avec la situation actuelle et, aux fins de cet exemple, les deux types peuvent également n’avoir aucune relation. Ce n’est qu’une des deux dernières lignes que nous utilisons le fait que le produit Derived* est convertible en Base* lorsque nous effectuons la conversion. Mais notez que cette conversion est une conversion de valeur réelle et que d n’est pas le même pointeur que static_cast(d) (au moins en ce qui concerne la langue).

Enfin, nettoyons ce gâchis:

 d->~Derived(); std::free(addr); 

L’opportunité de détruire l’original *b est passée, nous l’avons peut-être laissé filtrer.