Alguien lo mencionó en el IRC como el problema del corte.
Alguien lo mencionó en el IRC como el problema del corte.
Respuestas:
"Rebanar" es donde se asigna un objeto de una clase derivada a una instancia de una clase base, perdiendo así parte de la información, parte de la cual se "corta".
Por ejemplo,
class A {
int foo;
};
class B : public A {
int bar;
};
Entonces, un objeto de tipo B
tiene dos miembros de datos, foo
y bar
.
Entonces, si fueras a escribir esto:
B b;
A a = b;
Entonces la información b
sobre el miembro bar
se pierde en a
.
A a = b;
a
ahora es un objeto de tipo A
que tiene una copia B::foo
. Será un error devolverlo ahora, creo.
B b1; B b2; A& b2_ref = b2; b2 = b1
. Se podría pensar que haya copiado b1
a b2
, pero que no tienen! Ha copiado una parte de b1
a b2
(la parte de la b1
que B
heredó de A
), y dejó las otras partes de b2
cambios. b2
ahora es una criatura frankensteiniana que consta de unos pocos fragmentos b1
seguidos de algunos fragmentos de b2
. Ugh! Voto negativo porque creo que la respuesta es muy engañosa.
B b1; B b2; A& b2_ref = b2; b2_ref = b1
" El verdadero problema se produce si usted " ... deriva de una clase con un operador de asignación no virtual. ¿ A
Incluso está destinado a la derivación? No tiene funciones virtuales. Si deriva de un tipo, ¡tiene que lidiar con el hecho de que sus funciones miembro pueden ser llamadas!
La mayoría de las respuestas aquí no explican cuál es el problema real con el corte. Solo explican los casos benignos de rebanar, no los traicioneros. Suponga, como las otras respuestas, que está tratando con dos clases A
y de B
dónde B
deriva (públicamente) A
.
En esta situación, C ++ permite pasar una instancia de B
a A
's operador de asignación (y también al constructor de copia). Esto funciona porque una instancia de B
se puede convertir en a const A&
, que es lo que los operadores de asignación y los constructores de copias esperan que sean sus argumentos.
B b;
A a = b;
No sucede nada malo: solicitó una instancia de la A
cual es una copia B
, y eso es exactamente lo que obtiene. Claro, a
no contendrá algunos de b
los miembros de, pero ¿cómo debería? Es un A
, después de todo, no un B
, por lo que ni siquiera ha oído hablar de estos miembros, y mucho menos podría almacenarlos.
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
Puede pensar que b2
será una copia de b1
después. Pero, por desgracia, es no ! Si lo inspeccionas, descubrirás que b2
es una criatura frankensteiniana, hecha de algunos trozos de b1
(los trozos que B
hereda de A
), y algunos trozos de b2
(los trozos que solo B
contiene). ¡Ay!
¿Que pasó? Bueno, C ++ por defecto no trata a los operadores de asignación como virtual
. Por lo tanto, la línea a_ref = b1
llamará al operador de asignación de A
, no al de B
. Esto se debe a que, para funciones no virtuales, el tipo declarado (formalmente: estático ) (que es A&
) determina qué función se llama, en oposición al tipo real (formalmente: dinámico ) (que sería B
, ya que hace a_ref
referencia a una instancia de B
) . Ahora, A
el operador de asignación obviamente conoce solo los miembros declarados A
, por lo que copiará solo esos, dejando los miembros agregados B
sin cambios.
Asignar solo a partes de un objeto generalmente tiene poco sentido, pero C ++, desafortunadamente, no proporciona una forma integrada de prohibir esto. Sin embargo, puedes rodar el tuyo. El primer paso es hacer que el operador de asignación sea virtual . Esto garantizará que siempre se llame al operador de asignación del tipo real , no al tipo declarado . El segundo paso es usar dynamic_cast
para verificar que el objeto asignado tenga un tipo compatible. El tercer paso es hacer la asignación real en un miembro (protegida!) assign()
, Ya que B
's assign()
probablemente va a querer usar A
Es assign()
copiar A
's, miembros.
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
}
};
Tenga en cuenta que, por pura conveniencia, B
's operator=
anula covariantemente el tipo de retorno, ya que sabe que está devolviendo una instancia de B
.
derived
se puede dar cualquier valor al código que espera un base
valor, o se puede usar cualquier referencia derivada como referencia base. Me gustaría ver un lenguaje con un sistema de tipos que aborde ambos conceptos por separado. Hay muchos casos en los que una referencia derivada debe ser sustituible por una referencia base, pero las instancias derivadas no deben ser sustituibles por las base; También hay muchos casos en los que las instancias deberían ser convertibles, pero las referencias no deberían sustituir.
Si tiene una clase base A
y una clase derivada B
, puede hacer lo siguiente.
void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object "derived"
wantAnA(derived);
Ahora el método wantAnA
necesita una copia de derived
. Sin embargo, el objeto derived
no se puede copiar completamente, ya que la clase B
podría inventar variables miembro adicionales que no están en su clase base A
.
Por lo tanto, para llamar wantAnA
, el compilador "cortará" todos los miembros adicionales de la clase derivada. El resultado podría ser un objeto que no desea crear, porque
A
( B
se pierde todo el comportamiento especial de la clase ).wantAnA
(como su nombre lo indica) quiere un A
, entonces eso es lo que obtiene. Y una instancia de A
, se comportará como un A
. ¿Cómo es eso sorprendente?
derived
al tipo A
. La conversión implícita siempre es una fuente de comportamiento inesperado en C ++, porque a menudo es difícil de entender al mirar el código localmente que se realizó una conversión.
Estas son todas buenas respuestas. Solo me gustaría agregar un ejemplo de ejecución al pasar objetos por valor vs por referencia:
#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());
}
El resultado es:
Call by reference
I am a 'B'
Call by copy
'A' copy constructor
I am an 'A'
La tercera coincidencia en google para "C ++ slicing" me da este artículo de Wikipedia http://en.wikipedia.org/wiki/Object_slicing y esto (acalorado, pero las primeras publicaciones definen el problema): http://bytes.com/ forum / thread163565.html
Entonces es cuando asigna un objeto de una subclase a la superclase. La superclase no sabe nada de la información adicional en la subclase, y no tiene espacio para almacenarla, por lo que la información adicional se "corta".
Si esos enlaces no brindan suficiente información para una "buena respuesta", edite su pregunta para informarnos qué más está buscando.
El problema de segmentación es grave porque puede provocar daños en la memoria y es muy difícil garantizar que un programa no lo sufra. Para diseñarlo fuera del lenguaje, las clases que admiten herencia deben ser accesibles solo por referencia (no por valor). El lenguaje de programación D tiene esta propiedad.
Considere la clase A y la clase B derivadas de A. La corrupción de la memoria puede ocurrir si la parte A tiene un puntero p, y una instancia B que apunta p a los datos adicionales de B. Luego, cuando los datos adicionales se cortan, p apunta a la basura.
Derived
es convertible implícitamente a Base
). Esto obviamente es contrario al Principio Abierto-Cerrado, y una gran carga de mantenimiento.
En C ++, un objeto de clase derivada puede asignarse a un objeto de clase base, pero a la inversa no es posible.
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
}
La división de objetos ocurre cuando un objeto de clase derivada se asigna a un objeto de clase base, los atributos adicionales de un objeto de clase derivada se cortan para formar el objeto de clase base.
El problema de corte en C ++ surge de la semántica de valor de sus objetos, que se mantuvo principalmente debido a la compatibilidad con estructuras de C. Debe usar una referencia explícita o una sintaxis de puntero para lograr un comportamiento de objeto "normal" que se encuentra en la mayoría de los otros lenguajes que hacen objetos, es decir, los objetos siempre se pasan por referencia.
Las respuestas cortas son que corta el objeto asignando un objeto derivado a un objeto base por valor , es decir, el objeto restante es solo una parte del objeto derivado. Para preservar la semántica de valor, el corte es un comportamiento razonable y tiene usos relativamente raros, que no existen en la mayoría de los otros idiomas. Algunas personas lo consideran una característica de C ++, mientras que muchos lo consideran una de las peculiaridades / errores de C ++.
struct
, compatibilidad u otra falta de sentido que cualquier sacerdote aleatorio de OOP te dijo.
Base
debe tomar exactamente sizeof(Base)
bytes en la memoria, con una posible alineación, tal vez, es por eso que "asignación" (copia en pila ) no copiará a los miembros de clase derivados, sus desplazamientos están fuera de sizeof. Para evitar la "pérdida de datos", simplemente use el puntero, como cualquier otra persona, ya que la memoria del puntero está fija en su lugar y tamaño, mientras que la pila es muy volátil
Entonces ... ¿Por qué es malo perder la información derivada? ... porque el autor de la clase derivada puede haber cambiado la representación de modo que cortar la información adicional cambia el valor que representa el objeto. Esto puede suceder si la clase derivada se usa para almacenar en caché una representación que es más eficiente para ciertas operaciones, pero costosa de transformar a la representación base.
También pensé que alguien debería mencionar lo que debe hacer para evitar cortar ... Obtenga una copia de los estándares de codificación de C ++, 101 pautas de reglas y las mejores prácticas. Tratar con rebanar es el # 54.
Sugiere un patrón algo sofisticado para tratar completamente el problema: tener un constructor de copia protegida, un DoClone virtual puro protegido y un Clone público con una afirmación que le dirá si una clase derivada (adicional) no pudo implementar DoClone correctamente. (El método Clone hace una copia profunda adecuada del objeto polimórfico).
También puede marcar el constructor de copia en la base explícita, lo que permite un corte explícito si se desea.
1. LA DEFINICIÓN DEL PROBLEMA DE CORTE
Si D es una clase derivada de la clase base B, puede asignar un objeto de tipo Derivado a una variable (o parámetro) de tipo Base.
EJEMPLO
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
Aunque se permite la asignación anterior, el valor que se asigna a la mascota variable pierde su campo de raza. Esto se llama el problema de corte .
2. CÓMO ARREGLAR EL PROBLEMA DE CORTE
Para vencer el problema, utilizamos punteros a variables dinámicas.
EJEMPLO
Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed;
En este caso, ninguno de los miembros de datos o funciones de miembro de la variable dinámica a la que apunta ptrD (objeto de clase descendiente) se perderá. Además, si necesita usar funciones, la función debe ser una función virtual.
dog
eso no sea parte de la clase Pet
(el breed
miembro de datos) no se copie en la variable pet
? El código solo está interesado en los Pet
miembros de datos, aparentemente. Rebanar es definitivamente un "problema" si no es deseado, pero no lo veo aquí.
((Dog *)ptrP)
" Sugiero usarstatic_cast<Dog*>(ptrP)
Dog::breed
) no es un ERROR relacionado con SLICING?
Me parece que cortar en rodajas no es tanto un problema que no sea cuando sus propias clases y programas están mal diseñados / diseñados.
Si paso un objeto de subclase como parámetro a un método, que toma un parámetro de tipo superclase, ciertamente debería tenerlo en cuenta y conocerlo internamente, el método llamado solo funcionará con el objeto de superclase (también conocido como clase base).
Me parece solo la expectativa irracional de que proporcionar una subclase donde se solicita una clase base, de alguna manera resultaría en resultados específicos de la subclase, causaría que el corte sea un problema. Es un diseño deficiente en el uso del método o una implementación de subclase deficiente. Supongo que generalmente es el resultado de sacrificar un buen diseño de OOP en favor de la conveniencia o las ganancias de rendimiento.
OK, lo intentaré después de leer muchas publicaciones que explican la división de objetos, pero no cómo se vuelve problemático.
El escenario vicioso que puede provocar daños en la memoria es el siguiente:
La división significa que los datos agregados por una subclase se descartan cuando un objeto de la subclase se pasa o se devuelve por valor o desde una función que espera un objeto de clase base.
Explicación: Considere la siguiente declaración de clase:
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
Como las funciones de copia de clase base no saben nada sobre el derivado, solo se copia la parte base del derivado. Esto se conoce comúnmente como rebanar.
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;
}
cuando un objeto de clase derivada se asigna a un objeto de clase base, los atributos adicionales de un objeto de clase derivada se cortan (descartan) del objeto de clase 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
}
Cuando un objeto de clase derivada se asigna a un objeto de clase base, todos los miembros del objeto de clase derivada se copian en un objeto de clase base, excepto los miembros que no están presentes en la clase base. Estos miembros son cortados por el compilador. Esto se llama segmentación de objetos.
Aquí hay un ejemplo:
#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;
}
Generará:
[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'
Acabo de encontrarme con el problema del corte y rápidamente llegué aquí. Así que déjame agregar mis dos centavos a esto.
Veamos un ejemplo del "código de producción" (o algo parecido):
Digamos que tenemos algo que despacha acciones. Una interfaz de usuario del centro de control, por ejemplo.
Esta interfaz de usuario debe obtener una lista de las cosas que se pueden enviar actualmente. Entonces definimos una clase que contiene la información de despacho. Digamos que es Action
. Entonces an Action
tiene algunas variables miembro. Por simplicidad solo tenemos 2, siendo a std::string name
y a std::function<void()> f
. Luego tiene un void activate()
que simplemente ejecuta al f
miembro.
Entonces la interfaz de usuario obtiene un std::vector<Action>
suministro. Imagine algunas funciones como:
void push_back(Action toAdd);
Ahora hemos establecido cómo se ve desde la perspectiva de la interfaz de usuario. No hay problema hasta ahora. Pero otro tipo que trabaja en este proyecto de repente decide que hay acciones especializadas que necesitan más información en elAction
objeto. Por qué razón alguna vez. Eso también podría resolverse con capturas lambda. Este ejemplo no se toma 1-1 del código.
Entonces el chico deriva de Action
agregar su propio sabor.
Él pasa una instancia de su clase casera a la push_back
pero luego el programa se vuelve loco.
¿Entonces qué pasó?
Como usted podría haber adivinado: el objeto ha sido cortada.
La información adicional de la instancia se ha perdido y f
ahora es propensa a comportamientos indefinidos.
Espero que este ejemplo arroje luz para aquellas personas que realmente no pueden imaginarse las cosas cuando hablan de que A
s y B
s se derivan de alguna manera.