Quelqu'un l'a mentionné dans l'IRC comme étant le problème de découpage.
Quelqu'un l'a mentionné dans l'IRC comme étant le problème de découpage.
Réponses:
Le "découpage" 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 des informations - une partie est "découpée".
Par exemple,
class A {
int foo;
};
class B : public A {
int bar;
};
Un objet de type B
a donc deux membres de données, foo
et bar
.
Alors si vous deviez écrire ceci:
B b;
A a = b;
Les informations b
sur le membre bar
sont ensuite perdues a
.
A a = b;
a
est maintenant un objet de type A
qui a une copie de B::foo
. Ce sera une erreur de le rejeter maintenant, je pense.
B b1; B b2; A& b2_ref = b2; b2 = b1
. Vous pourriez penser que vous avez copié b1
à b2
, mais vous n'avez pas! Vous avez copié une partie de b1
à b2
(la partie b1
qui a B
hérité de A
), et laissé les autres parties b2
inchangées. b2
est maintenant une créature frankensteinienne composée de quelques morceaux de b1
suivi de quelques morceaux de b2
. Pouah! Downvoting parce que je pense que la réponse est très trompeuse.
B b1; B b2; A& b2_ref = b2; b2_ref = b1
" Le vrai problème se produit si vous " ... dérivez d'une classe avec un opérateur d'affectation non virtuel. Est A
même destiné à la dérivation? Il n'a pas de fonctions virtuelles. Si vous dérivez d'un type, vous devez faire face au fait que ses fonctions membres peuvent être appelées!
La plupart des réponses ici ne parviennent pas à expliquer quel est le problème réel avec le découpage. Ils n'expliquent que les cas bénins de tranchage, pas les cas perfides. Supposons, comme les autres réponses, que vous traitez avec deux classes A
et B
, d'où B
dérive (publiquement) A
.
Dans cette situation, C ++ vous permet de passer une instance de B
à A
l'opérateur d'affectation de »(et aussi au constructeur de copie). Cela fonctionne car une instance de B
peut être convertie en a const A&
, ce que les opérateurs d'affectation et les constructeurs de copies attendent de leurs arguments.
B b;
A a = b;
Il ne se passe rien de mal là-bas - vous avez demandé une instance de A
ce qui est une copie de B
, et c'est exactement ce que vous obtenez. Bien sûr, a
ne contiendra pas certains de b
ses membres, mais comment cela devrait-il? C'est un A
, après tout, pas un B
, donc il n'a même pas entendu parler de ces membres, et encore moins serait en mesure de les stocker.
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
Vous pourriez penser que ce b2
sera une copie de la b1
suite. Mais, hélas, ce n'est pas ! Si vous l'inspectez, vous découvrirez qu'il b2
s'agit d'une créature frankensteinienne, faite de quelques morceaux de b1
(les morceaux qui B
héritent de A
) et de quelques morceaux de b2
(les morceaux qui neB
contiennent que). Aie!
Qu'est-il arrivé? Eh bien, C ++ par défaut ne traite pas les opérateurs d'affectation comme virtual
. Ainsi, la ligne a_ref = b1
appellera l'opérateur d'affectation de A
, pas celui de B
. En effet, pour les fonctions non virtuelles, le type déclaré (formellement: statique ) (qui est A&
) détermine quelle fonction est appelée, par opposition au type réel (formellement: dynamique ) (qui le serait B
, car a_ref
référence une instance de B
) . Maintenant, A
l'opérateur d'affectation de ne connaît évidemment que les membres déclarés dans A
, donc il ne copiera que ceux-ci, laissant les membres ajoutés dansB
inchangés.
Affecter uniquement à des parties d'un objet n'a généralement pas de sens, mais C ++, malheureusement, ne fournit aucun moyen intégré pour l'interdire. Vous pouvez cependant lancer le vôtre. La première étape consiste à rendre l'opérateur d'affectation virtuel . Cela garantira que c'est toujours l' opérateur d'affectation du type réel qui est appelé, pas celui du type 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 à faire l'affectation réelle dans un (protégé!) Membre assign()
, depuis B
« s assign()
voudront probablement utiliser A
» s assign()
pour copier A
de 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, pour plus de commodité, B
s operator=
remplace de manière covariante le type de retour, car il sait qu'il retourne une instance de B
.
derived
valeur peut être donnée au code attendant une base
valeur, soit toute 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 type qui aborde les deux concepts séparément. Il existe de nombreux cas où une référence dérivée doit être substituable à une référence de base, mais les instances dérivées ne doivent pas être substituables à celles de base; il existe également de nombreux cas où les instances doivent être convertibles mais les références ne doivent pas se substituer.
Si vous avez une classe de base A
et une classe dérivée B
, vous pouvez effectuer les opérations suivantes.
void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object "derived"
wantAnA(derived);
Maintenant, la méthode a wantAnA
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 supplémentaires qui ne sont pas dans sa classe de base A
.
Par conséquent, pour appeler wantAnA
, le compilateur "découpera" tous les membres supplémentaires de la classe dérivée. Le résultat peut être un objet que vous ne souhaitez pas créer, car
A
-objet (tout comportement spécial de la classe B
est perdu).wantAnA
(comme son nom l'indique!) Veut un A
, alors c'est ce qu'il obtient. Et une instance de A
, se comportera, euh, comme un A
. Comment est-ce surprenant?
derived
le type A
. Le cast implicite est toujours une source de comportement inattendu en C ++, car il est souvent difficile de comprendre en regardant le code localement qu'un cast a eu lieu.
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'
Le troisième match dans google pour "C ++ slicing" me donne cet article Wikipedia http://en.wikipedia.org/wiki/Object_slicing et ce (chauffé, mais les premiers messages définissent le problème): http://bytes.com/ forum / thread163565.html
C'est donc lorsque vous affectez 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 de place pour les stocker, de sorte que les informations supplémentaires sont "découpées".
Si ces liens ne donnent pas suffisamment d'informations pour une "bonne réponse", veuillez modifier votre question pour nous faire savoir ce que vous recherchez de plus.
Le problème de découpage 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 hors du langage, les classes qui prennent en charge l'héritage doivent être accessibles par référence uniquement (et non par valeur). Le langage de programmation D possède cette propriété.
Considérez la classe A et la classe B dérivées de A. La corruption de mémoire peut se produire si la partie A a un pointeur p et une instance B qui pointe p vers les données supplémentaires de B. Ensuite, lorsque les données supplémentaires sont coupées, p pointe vers les ordures.
Derived
sont implicitement convertibles en Base
.)
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, les attributs supplémentaires d'un objet de classe dérivée sont découpés pour former l'objet de classe de base.
Le problème de découpage en C ++ provient de la sémantique de la valeur de ses objets, qui est restée principalement due à la compatibilité avec les structures C. Vous devez utiliser une référence explicite ou une syntaxe de pointeur pour obtenir un comportement d'objet "normal" trouvé dans la plupart des autres langages qui font 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 affectant 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 est un comportement raisonnable et a ses utilisations relativement rares, qui n'existent pas dans la plupart des autres langues. Certaines personnes le considèrent comme une caractéristique du C ++, tandis que beaucoup le considèrent comme l'une des bizarreries / défauts du C ++.
struct
, la compatibilité ou tout autre non-sens que tout prêtre OOP aléatoire vous a dit.
Base
doit prendre exactement des sizeof(Base)
octets en mémoire, avec un alignement possible, peut-être, c'est pourquoi "assignation" (copie sur pile ) ne copiera pas les membres de classe dérivés, leurs décalages sont en dehors de sizeof. Pour éviter de "perdre des données", utilisez simplement le pointeur, comme n'importe qui d'autre, car la mémoire du pointeur est fixée en place et en taille, tandis que la pile est très volatile
Alors ... Pourquoi la perte des informations dérivées est-elle mauvaise? ... car l'auteur de la classe dérivée peut avoir modifié la représentation de telle sorte que la suppression 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 à reconvertir en représentation de base.
Vous avez également pensé que quelqu'un devrait également mentionner ce que vous devez faire pour éviter de trancher ... Obtenez une copie des normes de codage C ++, des lignes directrices de 101 règles et des meilleures pratiques. Faire face au tranchage est # 54.
Il suggère un modèle quelque peu sophistiqué pour traiter entièrement le problème: avoir un constructeur de copie protégé, un DoClone virtuel pur protégé et un clone public avec une assertion qui vous dira si une classe dérivée (supplémentaire) n'a pas implémenté correctement DoClone. (La méthode Clone crée une copie complète appropriée de l'objet polymorphe.)
Vous pouvez également marquer le constructeur de copie sur la base explicite, ce qui permet un découpage explicite si vous le souhaitez.
1. LA DÉFINITION DU PROBLÈME DE TRANCHAGE
Si D est une classe dérivée de la classe de base B, vous pouvez affecter un objet de type Derived à 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 à l'animal variable perd son champ de race. C'est ce qu'on appelle le problème de tranchage .
2. COMMENT RÉSOUDRE LE PROBLÈME DE TRANCHAGE
Pour vaincre le problème, nous utilisons des pointeurs vers 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, la fonction doit être une fonction virtuelle.
dog
ne faisant pas partie de la classe Pet
(le breed
membre de données) ne soit pas copié dans la variable pet
? Le code ne s'intéresse qu'aux Pet
membres des données - apparemment. Le tranchage est définitivement un "problème" s'il n'est pas souhaité, mais je ne le vois pas ici.
((Dog *)ptrP)
" Je suggère d'utiliserstatic_cast<Dog*>(ptrP)
Dog::breed
) n'est en aucun cas une ERREUR liée à la COUPE?
Il me semble que le découpage n'est pas tant un problème que lorsque vos propres classes et programmes sont mal architecturés / conçus.
Si je passe un objet de sous-classe en tant que paramètre à une méthode, qui prend un paramètre de type superclasse, je devrais certainement en être conscient et connaître en interne, la méthode appelée fonctionnera uniquement avec l'objet superclasse (aka baseclass).
Il me semble que seule l'attente déraisonnable selon laquelle la fourniture d'une sous-classe dans laquelle une classe de base est demandée, aboutirait d'une manière ou d'une autre à des résultats spécifiques à la sous-classe, entraînerait un problème de découpage. C'est soit une mauvaise conception dans l'utilisation de la méthode, soit une mauvaise mise en œuvre de la sous-classe. Je suppose que c'est généralement le résultat de sacrifier une bonne conception de POO au profit d'opportunités ou de gains de performances.
OK, je vais essayer après avoir lu de nombreux articles expliquant le découpage d'objets, mais pas comment cela devient problématique.
Le scénario vicieux qui peut entraîner une corruption de la mémoire est le suivant:
Le découpage signifie que les données ajoutées par une sous-classe sont rejetées lorsqu'un objet de la sous-classe est transmis ou renvoyé par valeur ou à partir d'une fonction attend un objet de classe de base.
Explication: tenez compte de 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é découpage.
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;
}
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 coupés (supprimés) de l'objet de classe de base.
class Base {
int x;
};
class Derived : public Base {
int z;
};
int main()
{
Derived d;
Base b = d; // Object Slicing, z of d is sliced off
}
Lorsqu'un objet de classe dérivée est affecté à un objet de classe de base, tous les membres de l'objet de classe dérivée sont copiés dans l'objet de classe de base, à l'exception des membres qui ne sont pas présents dans la classe de base. Ces membres sont découpés par le compilateur. C'est ce qu'on appelle le découpage d'objets.
Voici un exemple:
#include<bits/stdc++.h>
using namespace std;
class Base
{
public:
int a;
int b;
int c;
Base()
{
a=10;
b=20;
c=30;
}
};
class Derived : public Base
{
public:
int d;
int e;
Derived()
{
d=40;
e=50;
}
};
int main()
{
Derived d;
cout<<d.a<<"\n";
cout<<d.b<<"\n";
cout<<d.c<<"\n";
cout<<d.d<<"\n";
cout<<d.e<<"\n";
Base b = d;
cout<<b.a<<"\n";
cout<<b.b<<"\n";
cout<<b.c<<"\n";
cout<<b.d<<"\n";
cout<<b.e<<"\n";
return 0;
}
Il générera:
[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'
J'ai juste traversé le problème du tranchage et j'ai rapidement atterri ici. Permettez-moi donc d'ajouter mes deux cents à cela.
Prenons un exemple du "code de production" (ou quelque chose qui se rapproche un peu):
Disons que nous avons quelque chose qui envoie des actions. Une interface utilisateur de centre de contrôle par exemple.
Cette interface utilisateur doit obtenir une liste des éléments qui peuvent actuellement être envoyés. Nous définissons donc une classe qui contient les informations de répartition. Appelons ça Action
. Donc, Action
a quelques variables membres. Pour plus de simplicité, nous avons juste 2, étant un std::string name
et un std::function<void()> f
. Ensuite, il a un void activate()
qui exécute simplement le f
membre.
L'interface utilisateur est donc std::vector<Action>
fournie. Imaginez quelques fonctions comme:
void push_back(Action toAdd);
Nous avons maintenant établi à quoi cela ressemble du point de vue de l'interface utilisateur. Aucun problème jusqu'ici. Mais un autre gars qui travaille sur ce projet décide soudainement qu'il existe des actions spécialisées qui nécessitent plus d'informations dans l' Action
objet. Pour quelle raison jamais. Cela pourrait également être résolu avec des captures lambda. Cet exemple n'est pas pris 1-1 du code.
Donc, le gars dérive Action
pour ajouter sa propre saveur.
Il passe une instance de sa classe brassée à la maison au push_back
mais ensuite le programme se détraque.
Alors, qu'est-ce-qu'il s'est passé?
Comme vous l'avez peut- être deviné: l'objet a été découpé.
Les informations supplémentaires de l'instance ont été perdues et f
sont désormais sujettes à un comportement non défini.
J'espère que cet exemple apportera de la lumière aux personnes qui ne peuvent pas vraiment imaginer des choses en parlant de A
s et B
s dérivant d'une manière ou d'une autre.