Comportement non défini lors de la réinitialisation d’un object via un nouvel emplacement sur ce pointeur

J’ai vu une présentation sur cppcon de Piotr Padlewski disant que le comportement suivant est indéfini:

int test(Base* a){ int sum = 0; sum += a->foo(); sum += a->foo(); return sum; } int Base::foo(){ new (this) Derived; return 1; } 

Remarque: Supposons sizeof(Base) == sizeof(Derived) et foo est virtuel.

Évidemment c’est mauvais, mais je m’intéresse à POURQUOI c’est UB. Je comprends que l’UB accède à un pointeur realloc mais il dit que c’est la même chose.

Questions connexes: Le comportement indéfini de `new (this) MyClass ();` après l’appel direct du destructeur? où il est dit “ok si pas d’exceptions” Est-il valide d’appeler directement un destructeur (virtuel)? Où il est dit new (this) MyClass(); résultats en UB. (contrairement à la question ci-dessus)

C ++ La construction d’object deux fois à l’aide de l’emplacement est-elle un nouveau comportement non défini? ça dit:

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.

ce qui semble encore être ok.

J’ai trouvé une autre description du placement nouveau dans le placement nouveau et l’affectation de la classe avec membre const

Si, après la fin de la durée de 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 initial, un pointeur pointant sur l’object initial, une référence 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 qualificatifs cv de niveau supérieur), et

  • le type de l’object d’origine n’est pas qualifié de const et, s’il s’agit d’un type de classe, ne contient aucun membre de données non statique dont le type est qualifié de const ou un type de référence, et

  • l’object d’origine était un object le plus dérivé du type T et le nouvel object était un object le plus dérivé du type T (en d’autres termes, il ne s’agissait pas de sous-objects de classe de base).

Cela semble expliquer l’UB. Mais est-ce vraiment vrai?

Est-ce que cela ne signifie pas que je ne pourrais pas avoir un std::vector ? Parce que je suppose qu’en raison de sa pré-allocation, std::vector doit s’appuyer sur placement-new s et sur des ctors explicites. Et le point 4 exige que ce soit le type le plus dérivé que Base ne soit clairement pas.

Je crois que Elizabeth Barret Browning l’a dit le mieux. Laisse-moi compter les chemins.

  1. Si Base n’est pas destructible de manière sortingviale, nous ne parvenons pas à nettoyer les ressources.
  2. Si sizeof(Derived) est supérieur à la taille du type dynamic, nous allons surcharger la mémoire.
  3. Si Base n’est pas le premier sous-object de Derived , le stockage du nouvel object ne recouvre pas exactement le stockage d’origine et vous finissez également par surcharger votre mémoire.
  4. Si Derived est simplement un type différent du type dynamic initial, même si sa taille est identique, l’object sur lequel nous appelons foo() ne peut pas être utilisé pour faire référence au nouvel object. Il en va de même si l’un des membres de Base ou Derived est qualifié qualifié ou est une référence. Vous auriez besoin de std::launder tous les pointeurs / références externes.

Cependant, si sizeof(Base) == sizeof(Derived) et que Derived est sortingvialement destructible, Base est le premier sous-object de Derived et que vous ne disposez que d’objects Derived … c’est très bien.

Concernant ta question

… Parce que je suppose qu’en raison de sa pré-allocation, std :: vector doit s’appuyer sur placement-news et sur les ctors explicites. Et le point 4 exige que ce soit le type le plus dérivé que Base ne soit clairement pas. Et le point 4 exige que ce soit le type le plus dérivé que Base ne soit clairement pas.

, Je pense que le malentendu vient de l’expression “object le plus dérivé” ou “type le plus dérivé”:

Le “type le plus dérivé” d’un object de type classe est la classe avec laquelle l’object a été instancié, que cette classe comporte d’autres sous-classes ou non. Considérez le programme suivant:

 struct A { virtual void foo() { cout << "A" << endl; }; }; struct B : public A { virtual void foo() { cout << "B" << endl; }; }; struct C : public B { virtual void foo() { cout << "C" << endl; }; }; int main() { B b; // b is-a B, but it also is-an A (referred to as a base object of b). // The most derived class of b is, however, B, and not A and not C. } 

Lorsque vous créez maintenant un vector , les éléments de ce vecteur seront des instances de la classe B et le type le plus dérivé des éléments sera toujours B , et non C (ou Derived ) dans votre cas.

J'espère que cela apporte un peu de lumière po