43 votes

Est-il possible de changer la classe d'un objet C++ après son instanciation ?

J'ai un groupe de classes qui héritent toutes des mêmes attributs d'une classe de base commune. La classe de base implémente quelques fonctions virtuelles qui fonctionnent dans des cas généraux, tandis que chaque sous-classe ré-implémente ces fonctions virtuelles pour une variété de cas spéciaux.

Voici la situation : Je veux que la spécificité de ces objets sous-classés soit sacrifiée. Essentiellement, j'aimerais implémenter un objet expend() qui permet à un objet de perdre son identité de sous-classe et de redevenir une instance de la classe de base avec les comportements généraux mis en œuvre dans la classe de base.

Je dois noter que les classes dérivées n'introduisent pas de variables supplémentaires, donc les classes de base et dérivées devraient avoir la même taille en mémoire.

Je suis prêt à détruire l'ancien objet et à en créer un nouveau, à condition que je puisse créer le nouvel objet à la même adresse mémoire, afin que les pointeurs existants ne soient pas cassés.

La tentative suivante ne fonctionne pas, et produit un comportement apparemment inattendu. Qu'est-ce qui m'échappe ici ?

#include <iostream>

class Base {
public:
    virtual void whoami() { 
        std::cout << "I am Base\n"; 
    }
};

class Derived : public Base {
public:
    void whoami() {
        std::cout << "I am Derived\n";
    }
};

Base* object;

int main() {
    object = new Derived; //assign a new Derived class instance
    object->whoami(); //this prints "I am Derived"

    Base baseObject;
    *object = baseObject; //reassign existing object to a different type
    object->whoami(); //but it *STILL* prints "I am Derived" (!)

    return 0;
}

3 votes

N'oubliez pas que le C++ n'est pas un langage dynamique, une fois qu'il est compilé, tout est fixé.

10 votes

Ce que vous semblez demander est une application du nouveau placement, mais je ne fournirai pas de réponse -- c'est une mauvaise conception IMO.

7 votes

Peut-être que vous pouvez utiliser la panthère de conception de stratégie au lieu d'une hiérarchie de classe

34voto

Jean-Bernard Jansen Points 4724

Vous pouvez le faire au prix du non-respect des bonnes pratiques et du maintien d'un code non sécurisé. D'autres réponses vous fourniront des astuces désagréables pour y parvenir.

Je n'aime pas les réponses qui se contentent de dire "vous ne devriez pas faire ça", mais j'aimerais suggérer qu'il existe probablement une meilleure façon d'obtenir le résultat que vous recherchez.

Le site schéma stratégique comme suggéré dans un commentaire de @manni66 est une bonne solution.

Vous devez également penser à conception orientée données puisque la hiérarchie des classes ne semble pas être un choix judicieux dans votre cas.

0 votes

Quand il fait *object = baseObject, quelqu'un sait-il si la vtable de baseObject est copiée sur l'objet dérivé ? Si c'est le cas, alors quand il appelle object->whoamI(), cela ne va-t-il pas appeler la version de base de la fonction ?

0 votes

Peu importe, ça ne l'est évidemment pas. Dans mon implémentation, la table virtuelle semble être de 8 octets et elle n'est pas copiée. Il est intéressant de noter que si je memcpy sizeof(Base) au lieu de l'assigner, il appelle la version Base. Cela semble être une fonctionnalité intéressante à avoir.

7 votes

@TitoneMaurice : J'ai entendu Undefined Behavior être décrit à l'aide de nombreuses invectives, mais c'est la première fois que je l'entends qualifier de "cool".

15voto

StoryTeller Points 6139

Oui et non. Une classe C++ définit le type d'une région de mémoire qui est un objet. Une fois que la région de mémoire a été instanciée, son type est défini. Vous pouvez essayer de travailler autour de le système de type bien sûr, mais le compilateur ne vous laissera pas vous en tirer comme ça. Tôt ou tard, il vous tirera dans le pied, parce que le compilateur a fait une hypothèse sur les types que vous avez violée, et il n'y a aucun moyen d'empêcher le compilateur de faire une telle hypothèse de manière portable.

Il existe cependant un modèle de conception pour cela : C'est le "State". Vous extrayez ce qui change dans sa propre hiérarchie de classes, avec sa propre classe de base, et vous faites en sorte que vos objets stockent un pointeur vers la base d'état abstraite de cette nouvelle hiérarchie. Vous pouvez ensuite les échanger à votre guise.

1 votes

Je pense que le modèle que vous décrivez est plus communément appelé la schéma stratégique

0 votes

@QPaysTaxes - C'est comme ça que le GdF l'appelait. Mais ils sont étroitement liés. De toute façon, je croyais que le PO voulait changer la disposition des objets, c'est pourquoi j'ai nommé que modèle. Je vais le laisser tel quel pour varier.

0 votes

C'est juste. Je voulais juste que les gens aient à leur disposition une explication plus longue et plus approfondie :)

15voto

Bathsheba Points 23209

Non, il n'est pas possible de changer le type d'un objet une fois instancié.

*object = baseObject; ne change pas le type de object il appelle simplement un opérateur d'affectation généré par le compilateur.

Cela aurait été une autre affaire si vous aviez écrit

object = new Base;

(en n'oubliant pas d'appeler delete naturellement ; actuellement votre code fait fuir un objet).

À partir de C++11, vous avez la possibilité de déplacer l'élément ressources d'un objet à un autre ; voir

http://en.cppreference.com/w/cpp/utility/move

0 votes

Mais cela changerait l'adresse de l'objet. Les pointeurs existants vers l'ancien objet pointeraient alors vers une mémoire non définie.

2 votes

Absolument. Mais rien ne vous empêcherait de construire une classe qui gère ce comportement. Vous pourriez même surcharger l'opérateur address-of.

0 votes

En fait, dans la plupart des compilateurs, vous pouvez écraser le pointeur de la table virtuelle, ce qui donnera exactement ce que veut le PO. Un bon truc pour impressionner vos amis, un horrible hack dans le code réel.

13voto

Ben Voigt Points 151460

Je suis prêt à détruire l'ancien objet et à en créer un nouveau, à condition que je puisse créer le nouvel objet à la même adresse mémoire, afin que les pointeurs existants ne soient pas cassés.

La norme C++ aborde explicitement cette idée dans la section 3.8 (Object Lifetime) :

Si après la fin de la durée de vie d'un objet et avant que l'espace de stockage qu'il occupait ne soit réutilisé ou libéré, un nouvel objet est créé à l'emplacement de stockage occupé par l'objet original, un pointeur indiquant l'objet original est créé. une référence qui faisait référence à l'objet original ou le nom de l'objet original fera automatiquement référence au nouvel objet, une fois que la durée de vie du nouvel objet aura commencé, peut être utilisé pour manipuler le nouvel objet <snip>

Oh wow, c'est exactement ce que tu voulais. Mais je n'ai pas montré toute la règle. Voici le reste :

si :

  • le stockage du nouvel objet recouvre exactement l'emplacement de stockage que l'objet initial occupait, et
  • le nouvel objet est du même type que l'objet original (en ignorant les qualificatifs cv de haut niveau). et
  • le type de l'objet original n'est pas qualifié de const et, s'il s'agit d'un type de classe, il ne contient pas de membre de données non statique dont le type est qualifié de const ou un type de référence, et
  • l'objet original était un objet le plus dérivé (1.8) de type T et le nouvel objet est un objet très dérivé de type T (c'est-à-dire qu'ils ne sont pas des sous-objets de la classe de base).

Votre idée a donc été prise en compte par le comité linguistique et rendue spécifiquement illégale, y compris la solution de rechange sournoise selon laquelle "j'ai un sous-objet de la classe de base du bon type, je vais simplement créer un nouvel objet à sa place", que le dernier point arrête net.

Vous pouvez remplacer un objet par un objet d'un type différent comme le montre la réponse de @RossRidge. Ou vous pouvez remplacer un objet et continuer à utiliser les pointeurs qui existaient avant le remplacement. Mais vous ne pouvez pas faire les deux ensemble.

Cependant, comme la célèbre citation : "Tout problème en informatique peut être résolu en ajoutant une couche d'indirection". et c'est vrai ici aussi.

Au lieu de votre méthode suggérée

Derived d;
Base* p = &d;
new (p) Base();  // makes p invalid!  Plus problems when d's destructor is automatically called

Vous pouvez le faire :

unique_ptr<Base> p = make_unique<Derived>();
p.reset(make_unique<Base>());

Si vous cachez ce pointeur et ce tour de main à l'intérieur d'une autre classe, vous aurez le "design pattern" tel que State ou Strategy mentionné dans d'autres réponses. Mais ils reposent tous sur un niveau supplémentaire d'indirection.

8voto

Ross Ridge Points 5534

Vous pouvez faire ce que vous demandez littéralement avec un placement new et un appel explicite au destructeur. Quelque chose comme ça :

#include <iostream>
#include <stdlib.h>

class Base {
public:
    virtual void whoami() { 
        std::cout << "I am Base\n"; 
    }
};

class Derived : public Base {
public:
    void whoami() {
        std::cout << "I am Derived\n";
    }
};

union Both {
    Base base;
    Derived derived;
};

Base *object;

int
main() {
    Both *tmp = (Both *) malloc(sizeof(Both));
    object = new(&tmp->base) Base;

    object->whoami(); 

    Base baseObject;
    tmp = (Both *) object;
    tmp->base.Base::~Base();
    new(&tmp->derived) Derived; 

    object->whoami(); 

    return 0;
}

Cependant, comme l'a dit Matb, ce n'est vraiment pas une bonne conception. Je vous recommande de reconsidérer ce que vous essayez de faire. Certaines des autres réponses ici pourraient aussi résoudre votre problème, mais je pense que tout ce qui va dans le sens de ce que vous demandez va être du bricolage. Vous devriez sérieusement envisager de concevoir votre application de manière à pouvoir changer le pointeur lorsque le type de l'objet change.

4 votes

J'ai envisagé (mais je ne l'ai pas fait) de voter contre parce que la première phrase est fausse. Vous pouvez no changement la classe d'un objet. On peut remplacer un objet par un autre d'une classe différente, comme vous le faites dans votre code.

3 votes

Je vote moins pour cette raison précise. Faites-moi savoir quand ce sera réparé.

1 votes

Cela déclenche-t-il un comportement indéfini ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X