903 votes

Qu'est-ce que le découpage en tranches d'objets ?

Quelqu'un l'a mentionné dans l'IRC comme étant le problème du tranchage.

726voto

David Dibben Points 7968

Le "découpage en tranches" consiste à affecter un objet d'une classe dérivée à une instance d'une classe de base, perdant ainsi une partie de l'information - une partie est "découpée en tranches".

Par exemple,

class A {
   int foo;
};

class B : public A {
   int bar;
};

Ainsi, un objet de type B a deux membres de données, foo y bar .

Alors si vous deviez écrire ceci :

B b;

A a = b;

Ensuite, les informations dans b à propos du membre bar est perdue dans a .

72 votes

Très instructif, mais voir stackoverflow.com/questions/274626#274636 pour un exemple de découpage en tranches lors d'appels de méthodes (qui souligne le danger un peu mieux que l'exemple d'une simple affectation).

66 votes

Intéressant. Je programme en C++ depuis 15 ans et ce problème ne m'a jamais effleuré, car j'ai toujours transmis les objets par référence pour des raisons d'efficacité et de style personnel. Cela montre à quel point les bonnes habitudes peuvent vous aider.

0 votes

@David : pouvez-vous expliquer votre ligne en détail ? Then the information in b about member bar is lost in a. . les données de foo sont-elles corrompues après l'affectation ? mais pourquoi ?

620voto

fgp Points 1949

La plupart des réponses n'expliquent pas quel est le problème réel du découpage en tranches. Elles n'expliquent que les cas bénins de découpage en tranches, et non les cas dangereux. Supposons, comme les autres réponses, que vous avez affaire à deux classes A y BB dérive (publiquement) de A .

Dans cette situation, le C++ vous permet de passer une instance de B à A (et aussi au constructeur de copie). Cela fonctionne parce qu'une instance de B peut être converti en un const A& ce qui est ce que les opérateurs d'affectation et les constructeurs de copie attendent de leurs arguments.

Le cas bénin

B b;
A a = b;

Rien de grave ne se produit ici - vous avez demandé une instance de A qui est une copie de B et c'est exactement ce que vous obtenez. Bien sûr, a ne contiendra pas certains des b mais comment le faire ? C'est un A après tout, pas un B donc ça n'a même pas entendu à propos de ces membres, et encore moins serait capable de les stocker.

L'affaire des traîtres

B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!

Vous pourriez penser que b2 sera une copie de b1 après. Mais, hélas, c'est no ! Si vous l'inspectez, vous découvrirez que b2 est une créature de Frankenstein, faite de quelques morceaux de b1 (les morceaux qui B hérite de A ), et quelques morceaux de b2 (les morceaux que seuls B contient). Aïe !

Que s'est-il passé ? Eh bien, par défaut, le C++ ne traite pas les opérateurs d'assignation comme étant virtual . Ainsi, la ligne a_ref = b1 appellera l'opérateur d'affectation de A et non celle de B . En effet, pour les fonctions non virtuelles, la fonction déclaré (formellement : statique ) (qui est A& ) détermine quelle fonction est appelée, par opposition à l'option réel (formellement : dynamique ) (qui serait B puisque a_ref fait référence à une instance de B ). Maintenant, A L'opérateur d'affectation de l'utilisateur ne connaît évidemment que les membres déclarés dans l'article A donc il ne copiera que ceux-là, laissant les membres ajoutés dans B inchangé.

Une solution

Le fait de n'assigner que des parties d'un objet n'a généralement pas beaucoup de sens, mais le C++ ne fournit malheureusement pas de moyen intégré pour l'interdire. Vous pouvez cependant en créer un. La première étape consiste à rendre l'opérateur d'affectation virtual . Cela garantira que c'est toujours le réel qui est appelé, et non l'opérateur d'affectation du type déclaré de type. La deuxième étape consiste à utiliser dynamic_cast pour vérifier que l'objet attribué a un type compatible. La troisième étape consiste à effectuer l'assignation réelle dans un membre (protégé !) assign() puisque B 's assign() vous voudrez probablement utiliser A 's assign() pour copier A des membres.

class A {
public:
  virtual A& operator= (const A& a) {
    assign(a);
    return *this;
  }

protected:
  void assign(const A& a) {
    // copy members of A from a to this
  }
};

class B : public A {
public:
  virtual B& operator= (const A& a) {
    if (const B* b = dynamic_cast<const B*>(&a))
      assign(*b);
    else
      throw bad_assignment();
    return *this;
  }

protected:
  void assign(const B& b) {
    A::assign(b); // Let A's assign() copy members of A from b to this
    // copy members of B from b to this
  }
};

Notez que, par pure commodité, B 's operator= remplace de manière covariante le type de retour, puisqu'il connaît qu'il retourne une instance de B .

1 votes

Alors les quelques opérations sur un objet de A ne sont pas autorisées lorsque l'objet est de type B .

16 votes

À mon avis, le problème est qu'il y a deux types différents de substituabilité qui peuvent être impliqués par l'héritage : soit tout type de substituabilité peut être considéré comme un substitut. derived peut être donnée au code qui attend une valeur base ou toute autre référence dérivée peut être utilisée comme référence de base. J'aimerais voir un langage avec un système de types qui traite les deux concepts séparément. Il y a de nombreux cas où une référence dérivée devrait être substituable à une référence de base, mais les instances dérivées ne devraient pas être substituables aux instances de base ; il y a aussi de nombreux cas où les instances devraient être convertibles mais les références ne devraient pas être substituables.

1 votes

Conceptuellement, dans .NET, si une fonction retourne KeyValuePair<SiameseCat, ToyotaPrius> on devrait être en mesure de stocker ce résultat dans un emplacement de stockage du type KeyValuePair<Animal, Vehicle> non pas parce qu'une instance de la première est une instance de la seconde, mais plutôt parce que l'interprétation de la valeur de retour en tant que KVP<A,V> le transformerait effectivement en un. Malheureusement, cela nécessiterait une hiérarchie distincte de celle de l'héritage normal, car une instance encadrée du premier type n'est absolument pas équivalente à une instance encadrée du second.

177voto

Black Points 3104

Si vous avez une classe de base A et une classe dérivée B Vous pouvez alors procéder comme suit.

void wantAnA(A myA)
{
   // work with myA
}

B derived;
// work with the object "derived"
wantAnA(derived);

Maintenant, la méthode wantAnA a besoin d'une copie de derived . Cependant, l'objet derived ne peut pas être copié complètement, car la classe B pourrait inventer des variables membres additionnelles qui ne sont pas dans sa classe de base A .

Par conséquent, pour appeler wantAnA le compilateur va "découper" tous les membres supplémentaires de la classe dérivée. Le résultat peut être un objet que vous ne vouliez pas créer, parce que

  • elle peut être incomplète,
  • il se comporte comme un A -objet (tous les comportements particuliers de la classe B est perdue).

55 votes

Le C++ est no Java ! Si wantAnA (comme son nom l'indique !) veut un A alors c'est ce qui arrive. Et une instance de A se comportera, euh, comme un A . Comment cela peut-il être surprenant ?

103 votes

@fgp : C'est surprenant, parce que vous ne pas passer un A à la fonction.

4 votes

@Black : Mais wantAnA l'a dit veut un A, donc c'est ce qu'il obtient. C'est la même chose que de déclarer une fonction pour prendre un int, passer 0.1, et ensuite se plaindre que la fonction reçoit 0...

36voto

Paul Points 18124

La troisième correspondance dans google pour "C++ slicing" me donne cet article de Wikipedia http://en.wikipedia.org/wiki/Object_slicing et ceci (chauffé, mais les premiers posts définissent le problème) : http://bytes.com/forum/thread163565.html

C'est donc quand on assigne un objet d'une sous-classe à la super-classe. La superclasse ne sait rien des informations supplémentaires de la sous-classe, et n'a pas la place de les stocker, donc les informations supplémentaires sont "coupées".

Si ces liens ne donnent pas suffisamment d'informations pour une "bonne réponse", veuillez modifier votre question pour nous indiquer ce que vous recherchez en plus.

34voto

Walter Bright Points 2967

Le problème du découpage en tranches est grave car il peut entraîner une corruption de la mémoire, et il est très difficile de garantir qu'un programme n'en souffre pas. Pour l'éliminer du langage, les classes qui supportent l'héritage doivent être accessibles uniquement par référence (et non par valeur). Le langage de programmation D possède cette propriété.

Considérons une classe A et une classe B dérivée de A. Une corruption de la mémoire peut se produire si la partie A possède un pointeur p et une instance B qui fait pointer p vers les données supplémentaires de B. Ensuite, lorsque les données supplémentaires sont coupées, p pointe vers des déchets. Ensuite, lorsque les données supplémentaires sont supprimées, p pointe vers un déchet.

3 votes

Veuillez expliquer comment la corruption de la mémoire peut se produire.

2 votes

Étant donné : classe A { virtual void foo() { } } ; classe B : A { int *p ; void foo() { *p = 3 ; } } ; Maintenant, coupez un B quand il est assigné à A, appelez foo(), qui appelle B::foo(), et voilà ! Corruption de mémoire en assignant une valeur résiduelle à B::p.

1 votes

Oui, Walter mélange l'affectation de pointeur (où A* pointe vers un objet B, donc B::p n'est PAS perdu) et l'affectation d'objet (après quoi A::foo est appelé).

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