web-dev-qa-db-fra.com

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

Quelqu'un l'a mentionné dans le IRC comme problème de découpage.

695
Frankomania

"Tranchage" est l'endroit où vous affectez 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 "coupée".

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 et bar.

Alors si vous deviez écrire ceci:

B b;

A a = b;

Ensuite, les informations contenues dans b à propos du membre bar sont perdues dans a.

575
David Dibben

La plupart des réponses ne parviennent pas à expliquer le problème réel du découpage en tranches. Ils expliquent uniquement les cas bénins de tranchage, pas les traîtres. Comme les autres réponses, supposons que vous ayez affaire à deux classes A et B, où B dérive (publiquement) de A.

Dans cette situation, C++ vous permet de passer une instance de B à l'opérateur d'affectation de A (ainsi qu'au constructeur de copie). Cela fonctionne car une instance de B peut être convertie en un const A&, ce que les opérateurs d'assignation et les constructeurs de copie attendent de leurs arguments.

Le cas bénin

B b;
A a = b;

Rien de mal ne s'y passe - 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 membres de b, mais comment devrait-il? C'est un A, après tout, pas un B, de sorte qu'il n'a même pas entendu à propos de ces membres, encore moins serait capable de les stocker.

Le cas perfide

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 par la suite. Mais, hélas, c'est pas! Si vous l'inspectez, vous découvrirez que b2 est une créature frankensteinienne constituée de morceaux de b1 (les morceaux que B hérite de A) et de morceaux de b2 (morceaux que seul B contient). Aie!

Qu'est-il arrivé? Eh bien, C++ par défaut ne traite pas les opérateurs d’assignation comme virtual. Ainsi, la ligne a_ref = b1 appellera l'opérateur d'affectation de A et non celui de B. En effet, pour les fonctions non virtuelles, le type déclaré (qui est A&) détermine quelle fonction est appelée, par opposition au type actuel (qui be B, puisque a_ref fait référence à une instance de B). Maintenant, l'opérateur d'affectation de A ne connaît évidemment que les membres déclarés dans A, il ne les copiera donc que, en laissant les membres ajoutés dans B inchangés.

Une solution

N'affecter que des parties d'un objet n'a généralement aucun sens, mais C++ ne fournit malheureusement aucun moyen intégré d'interdire cela. Vous pouvez cependant rouler le vôtre. La première étape consiste à rendre l'opérateur d'affectation virtuel . Cela garantira que c'est toujours l'opérateur de tâche réel du type qui est appelé, pas celui de déclaré. La deuxième étape consiste à utiliser dynamic_cast pour vérifier que l'objet affecté a un type compatible. La troisième étape consiste à effectuer l'affectation réelle dans un membre (protégé!) assign(), étant donné que assign() de B voudra probablement utiliser les assign() de A pour copier les membres de A.

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 souci de commodité, le operator= de B écrase de manière covariante le type de retour, puisqu'il sait qu'il renvoie une instance de B.

460
fgp

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 nécessite une copie de derived. Cependant, l'objet derived ne peut pas être copié complètement, car la classe B pourrait inventer des variables de membre supplémentaires qui ne sont pas dans sa classe de base A.

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

  • il peut être incomplet,
  • il se comporte comme un objet A- (tout comportement spécial de la classe B est perdu).
148
Black

Ce sont toutes de bonnes réponses. Je voudrais juste ajouter un exemple d'exécution lors du passage d'objets par valeur vs par référence:

#include <iostream>

using namespace std;

// Base class
class A {
public:
    A() {}
    A(const A& a) {
        cout << "'A' copy constructor" << endl;
    }
    virtual void run() const { cout << "I am an 'A'" << endl; }
};

// Derived class
class B: public A {
public:
    B():A() {}
    B(const B& a):A(a) {
        cout << "'B' copy constructor" << endl;
    }
    virtual void run() const { cout << "I am a 'B'" << endl; }
};

void g(const A & a) {
    a.run();
}

void h(const A a) {
    a.run();
}

int main() {
    cout << "Call by reference" << endl;
    g(B());
    cout << endl << "Call by copy" << endl;
    h(B());
}

La sortie est:

Call by reference
I am a 'B'

Call by copy
'A' copy constructor
I am an 'A'
38
geh

Le troisième match dans Google pour "découpage C++" me donne cet article Wikipedia http://en.wikipedia.org/wiki/Object_slicing et ceci (chauffé, mais les premiers messages définissent le problème): - http://bytes.com/forum/thread163565.html

Donc, c'est quand vous assignez un objet d'une sous-classe à la super classe. La superclasse ne sait rien des informations supplémentaires contenues dans la sous-classe et n'a pas la place de les stocker. Les informations supplémentaires sont donc "découpées en tranches".

Si ces liens ne donnent pas assez d'informations pour une "bonne réponse", éditez votre question afin de nous indiquer ce que vous cherchez.

31

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 le concevoir en dehors du langage, les classes prenant en charge l'héritage doivent être accessibles uniquement par référence (et non par valeur). Le langage de programmation D a cette propriété.

Considérez les classes A et B dérivées de A. Une corruption de mémoire peut survenir si la partie A a un pointeur p et une instance B qui pointe p sur les données supplémentaires de B. Ensuite, lorsque les données supplémentaires sont découpées, p pointe vers garbage.

28
Walter Bright

En C++, un objet de classe dérivé peut être affecté à un objet de classe de base, mais l'inverse n'est pas possible.

class Base { int x, y; };

class Derived : public Base { int z, w; };

int main() 
{
    Derived d;
    Base b = d; // Object Slicing,  z and w of d are sliced off
}

Le découpage d'objet se produit lorsqu'un objet de classe dérivée est affecté à un objet de classe de base, des attributs supplémentaires d'un objet de classe dérivée sont découpés en tranches pour former l'objet de classe de base.

9

Alors ... Pourquoi perdre l'information dérivée est-il mauvais? ... car l'auteur de la classe dérivée peut avoir modifié la représentation, de sorte que le découpage des informations supplémentaires modifie la valeur représentée par l'objet. Cela peut se produire si la classe dérivée est utilisée pour mettre en cache une représentation plus efficace pour certaines opérations mais coûteuse pour la reconvertir en représentation de base.

Pensez également que quelqu'un devrait également mentionner ce que vous devez faire pour éviter les découpages en tranches ... Obtenez une copie des normes de codage C++, des directives de règles 101 et des meilleures pratiques. Faire face au tranchage est le # 54.

Cela suggère un modèle assez sophistiqué pour traiter complètement le problème: avoir un constructeur de copie protégé, un DoClone virtuel pur et protégé, et un clone public avec une assertion qui vous indiquera si une classe (ultérieure) dérivée n'a pas correctement implémenté DoClone. (La méthode Clone crée une copie complète appropriée de l'objet polymorphe.)

Vous pouvez également marquer le constructeur de la copie sur la base de manière explicite, ce qui permet une découpe explicite si vous le souhaitez.

7
Steve Steiner

Le problème de découpage en C++ provient de la sémantique des valeurs de ses objets, qui est restée principalement due à la compatibilité avec les structures C. Vous devez utiliser une syntaxe de référence explicite ou un pointeur pour obtenir un comportement "normal" dans la plupart des autres langages utilisant des objets, c’est-à-dire que les objets sont toujours transmis par référence.

La réponse courte est que vous découpez l'objet en assignant un objet dérivé à un objet de base par valeur, c'est-à-dire que l'objet restant n'est qu'une partie de l'objet dérivé. Afin de préserver la sémantique des valeurs, le découpage en tranches est un comportement raisonnable et a des utilisations relativement rares, qui n'existent pas dans la plupart des autres langues. Certaines personnes le considèrent comme une fonctionnalité du C++, alors que d'autres le considéraient comme l'une des bizarreries/défauts du C++.

7
ididak

1. LA DÉFINITION DE PROBLÈME DE TRANCHE

Si D est une classe dérivée de la classe de base B, vous pouvez affecter un objet de type Dérivé à une variable (ou paramètre) de type Base.

EXEMPLE

class Pet
{
 public:
    string name;
};
class Dog : public Pet
{
public:
    string breed;
};

int main()
{   
    Dog dog;
    Pet pet;

    dog.name = "Tommy";
    dog.breed = "Kangal Dog";
    pet = dog;
    cout << pet.breed; //ERROR

Bien que l'affectation ci-dessus soit autorisée, la valeur affectée à la variable animal de compagnie perd son champ de race. C'est ce qu'on appelle le problème de tranchage.

2. COMMENT FIXER LE PROBLÈME DE TRANCHEMENT

Pour résoudre le problème, nous utilisons des pointeurs sur des variables dynamiques.

EXEMPLE

Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;         
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed; 

Dans ce cas, aucun des membres de données ou des fonctions membres de la variable dynamique pointée par ptrD (objet de classe descendant) ne sera perdu. De plus, si vous devez utiliser des fonctions, cette fonction doit être virtuelle.

7
haberdar

Il me semble que le découpage n’est pas un problème si ce n’est que lorsque vos classes et votre programme sont mal conçus/architecturés.

Si je passe un objet de sous-classe en tant que paramètre à une méthode, qui prend un paramètre de type superclass, je devrais certainement en être conscient et connaître le fonctionnement interne, la méthode appelée fonctionnera uniquement avec l'objet superclass (aka baseclass).

Il me semble que seule l'attente déraisonnable selon laquelle le fait de fournir une sous-classe lorsqu'une classe de base est demandée, aboutirait d'une manière ou d'une autre à des résultats spécifiques à une sous-classe, poserait le problème du découpage en tranches. Il s’agit soit d’une conception médiocre dans l’utilisation de la méthode, soit d’une implémentation médiocre dans une sous-classe. J'imagine que c'est généralement le résultat de sacrifier le bon OOP - conception en faveur d'opportunités ou de gains de performances.

4
Minok

Le découpage en tranches signifie que les données ajoutées par une sous-classe sont ignorées lorsqu'un objet de la sous-classe est transmis ou renvoyé par valeur ou par une fonction qui attend un objet de classe de base.

Explication: Considérons la déclaration de classe suivante:

           class baseclass
          {
                 ...
                 baseclass & operator =(const baseclass&);
                 baseclass(const baseclass&);
          }
          void function( )
          {
                baseclass obj1=m;
                obj1=m;
          }

Comme les fonctions de copie de classe de base ne savent rien du dérivé, seule la partie de base du dérivé est copiée. Ceci est communément appelé tranchage.

3
Santosh

OK, je vais l'essayer après avoir lu de nombreux articles expliquant le découpage d'objet, mais pas comment cela devient problématique.

Le scénario vicieux pouvant entraîner une corruption de la mémoire est le suivant:

  • Class fournit une affectation (accidentelle, éventuellement générée par le compilateur) sur une classe de base polymorphe.
  • Le client copie et découpe une instance d'une classe dérivée.
  • Le client appelle une fonction membre virtuelle qui accède à l'état découpé.
3
Dude
class A 
{ 
    int x; 
};  

class B 
{ 
    B( ) : x(1), c('a') { } 
    int x; 
    char c; 
};  

int main( ) 
{ 
    A a; 
    B b; 
    a = b;     // b.c == 'a' is "sliced" off
    return 0; 
}
2
quidkid