¿Qué es rebanar objetos?
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 ella 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 tuvieras que escribir esto:
B b;
A a = b;
Entonces la información b
sobre el miembro bar
se pierde en a
.
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, al igual que las otras respuestas, que está tratando con dos clases A
y B
, de dónde B
deriva (públicamente) A
.
En esta situación, C ++ le permite pasar una instancia del operador de asignación de B
to A
(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 copia esperan que sean sus argumentos.
El caso benigno
B b;
A a = b;
No sucede nada malo allí: solicitó una instancia de la A
cual es una copia B
, y eso es exactamente lo que obtiene. Seguro, a
no contendrá algunos de b
los miembros, pero ¿cómo debería hacerlo? Es una A
, después de todo, no una B
, por lo que ni siquiera ha oído acerca de estos miembros, y mucho menos sería capaz de almacenarlos.
El caso traicionero
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!
Podrías pensar que b2
será una copia de b1
después. Pero, por desgracia, es no ! Si lo inspecciona, descubrirá que b2
es una criatura de Frankenstein, hecha de algunos trozos de b1
(los trozos de los que B
hereda A
) y algunos trozos de b2
(los trozos que solo B
contiene). ¡Ay!
¿Qué sucedió? 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 las funciones no virtuales, el tipo declarado (formalmente: estático ) (que es A&
) determina qué función se llama, a diferencia del 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 de, obviamente, solo conoce los miembros declarados A
, por lo que copiará solo esos, dejando los miembros agregados B
sin cambios.
Una solución
La asignación solo a partes de un objeto generalmente tiene poco sentido, pero C ++, desafortunadamente, no proporciona una forma incorporada de prohibir esto. Sin embargo, puede enrollar el suyo. 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 utilizar dynamic_cast
para verificar que el objeto asignado tiene un tipo compatible. El tercer paso es hacer la asignación real en un miembro (¡protegido!) assign()
, Ya que B
's assign()
probablemente querrá usar A
' s assign()
para 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 covariablemente el tipo de retorno, ya que sabe que está devolviendo una instancia de B
.
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 por completo, 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 deseaba crear, porque
- puede estar incompleto,
- se comporta como un
A
-objeto (B
se pierde todo comportamiento especial de la clase ).
Todas estas son 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());
}
La salida es:
Call by reference
I am a 'B'
Call by copy
'A' copy constructor
I am an 'A'
La tercera coincidencia en Google para "segmentación de C ++" 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 hacernos saber qué más está buscando.
El problema de la 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 la 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 derivada 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 basura.
En C ++, un objeto de clase derivada se puede asignar a un objeto de clase base, pero no es posible al revés.
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 se asigna un objeto de clase derivada a un objeto de clase base, los atributos adicionales de un objeto de clase derivada se cortan para formar el objeto de clase base.
Entonces ... ¿Por qué es malo perder la información derivada? ... porque el autor de la clase derivada puede haber cambiado la representación de tal manera 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 de nuevo a la representación base.
También pensé que alguien también debería mencionar lo que debe hacer para evitar cortar ... Obtenga una copia de los estándares de codificación C ++, las directrices de 101 reglas y las mejores prácticas. Lidiar con el corte es el # 54.
Sugiere un patrón algo sofisticado para abordar completamente el problema: tenga un constructor de copia protegido, 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 como explícito, lo que permite el corte explícito si lo desea.
El problema de la división en C ++ surge de la semántica de valor de sus objetos, que se mantuvo principalmente debido a la compatibilidad con las estructuras de C. Necesita usar una referencia explícita o sintaxis de puntero para lograr el comportamiento "normal" del objeto que se encuentra en la mayoría de los otros lenguajes que utilizan objetos, es decir, los objetos siempre se pasan por referencia.
La respuesta corta es 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 del valor, la segmentación es un comportamiento razonable y tiene usos relativamente raros, que no existen en la mayoría de los otros lenguajes. Algunas personas lo consideran una característica de C ++, mientras que muchos lo consideran una de las peculiaridades / fallas de C ++.
1. LA DEFINICIÓN DE PROBLEMA DE REBANADO
Si D es una clase derivada de la clase base B, entonces puede asignar un objeto de tipo Derived 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 variable mascota pierde su campo de raza. A esto se le llama el problema del corte .
2. CÓMO SOLUCIONAR EL PROBLEMA DEL REBANADO
Para vencer el problema, usamos 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, no se perderá ninguno de los miembros de datos o funciones miembro de la variable dinámica a la que apunta ptrD (objeto de clase descendiente). Además, si necesita utilizar funciones, la función debe ser una función virtual.
Me parece que la división no es un problema tanto más que 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 ser consciente de eso y saber internamente, el método llamado funcionará solo con el objeto de superclase (también conocido como clase base).
Me parece que solo la expectativa irrazonable de que proporcionar una subclase donde se solicita una clase base, de alguna manera daría como resultado resultados específicos de 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 suele ser el resultado de sacrificar un buen diseño de programación orientada a objetos en favor de la conveniencia o la mejora del rendimiento.
Bien, lo intentaré después de leer muchas publicaciones que explican el corte de objetos, pero no cómo se vuelve problemático.
El escenario vicioso que puede resultar en la corrupción de la memoria es el siguiente:
- La clase proporciona asignación (accidentalmente, posiblemente generada por el compilador) en una clase base polimórfica.
- El cliente copia y corta una instancia de una clase derivada.
- El cliente llama a una función de miembro virtual que accede al estado cortado.
Cortar 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 la 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;
}
Veo que todas las respuestas mencionan cuando la división de objetos ocurre cuando se cortan los miembros de datos. Aquí doy un ejemplo de que los métodos no se anulan:
class A{
public:
virtual void Say(){
std::cout<<"I am A"<<std::endl;
}
};
class B: public A{
public:
void Say() override{
std::cout<<"I am B"<<std::endl;
}
};
int main(){
B b;
A a1;
A a2=b;
b.Say(); // I am B
a1.Say(); // I am A
a2.Say(); // I am A why???
}
B (objeto b) se deriva de A (objeto a1 y a2). by a1, como esperamos, llaman a su función miembro. Pero desde el punto de vista del polimorfismo, no esperamos que a2, que es asignado por b, no sea anulado. Básicamente, a2 solo guarda una parte de clase A de b y eso es la división de objetos en C ++.
Para resolver este problema, se debe utilizar una referencia o un puntero.
A& a2=b;
a2.Say(); // I am B
o
A* a2 = &b;
a2->Say(); // I am B
Para más detalles, vea mi publicación.