c++ inheritance c++-faq object-slicing

c++ - ¿Qué es rebanar objetos?



inheritance c++-faq (17)

"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 "rebana".

Por ejemplo,

class A { int foo; }; class B : public A { int bar; };

Así que 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 en b sobre la bar miembros se pierde en a .

Alguien lo mencionó en el IRC, pero Google no tiene una buena respuesta.


Acabo de encontrarme con el problema de rebanar y rápidamente aterricé aquí. Así que déjame agregar mis dos centavos a esto.

Vamos a tener un ejemplo de "código de producción" (o algo que se acerca un poco):

Digamos que tenemos algo que despacha acciones. Una interfaz de usuario del centro de control, por ejemplo.
Esta interfaz de usuario necesita obtener una lista de las cosas que actualmente se pueden enviar. Así que definimos una clase que contiene la información de envío. Llamémoslo Action . Así que una Action tiene algunas variables miembro. Para simplificar, solo tenemos 2, siendo std::string name y std::function<void()> f . Luego tiene un void activate() que simplemente ejecuta el miembro f .

Así que la interfaz de usuario obtiene un std::vector<Action> suministrado. Imagina 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 otra persona que trabaja en este proyecto de repente decide que hay acciones especializadas que necesitan más información en el objeto Action . Por lo que nunca. Eso también podría ser resuelto con capturas lambda. Este ejemplo no se toma 1-1 del código.

Así que el chico deriva de la Action para agregar su propio sabor.
Pasa una instancia de su clase de push_back al push_back pero luego el programa se vuelve loco.

¿Entonces qué pasó?
Como habrás adivinado: el objeto ha sido cortado.

La información adicional de la instancia se ha perdido, y f ahora es propenso a un comportamiento indefinido.

Espero que este ejemplo sirva de luz para aquellas personas que realmente no pueden imaginar las cosas cuando hablan de que A y B se derivan de alguna manera.


Cuando un objeto de clase derivado se asigna a un objeto de clase base, todos los miembros del objeto de clase derivado se copian en el 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; }

Se generará:

[Error] ''class Base'' has no member named ''d'' [Error] ''class Base'' has no member named ''e''


El problema de corte en C ++ surge de la semántica del valor de sus objetos, que se mantuvo principalmente debido a la compatibilidad con C structs. Debe usar la referencia explícita o la 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 es que usted divide el objeto asignando un objeto derivado a un objeto base por valor , es decir, el objeto restante es solo una parte del objeto derivado. Con el fin de preservar la semántica de valor, el corte es un comportamiento razonable y tiene sus usos relativamente raros, que no existe 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 ++.


El problema del corte en rodajas es grave porque puede provocar daños en la memoria y es muy difícil garantizar que un programa no sufra por ello. 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 la basura.


En C ++, un objeto de clase derivado puede asignarse a un objeto de clase base, pero la otra forma 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 segmentación de objetos ocurre cuando un objeto de clase derivado 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.


Encuentre respuestas similares aquí: http://sickprogrammersarea.blogspot.in/2014/03/technical-interview-questions-on-c_6.html

Cortar significa que los datos agregados por una subclase se descartan cuando un objeto de la subclase se pasa o 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 acerca de lo derivado, solo se copia la parte base de lo derivado. Esto se conoce comúnmente como rebanar.


Entonces ... ¿Por qué es malo perder la información derivada? ... porque el autor de la clase derivada puede haber cambiado la representación de manera que al cortar la información adicional se cambia el valor que representa el objeto. Esto puede suceder si la clase derivada se utiliza para almacenar en caché una representación que es más eficiente para ciertas operaciones, pero que es costoso volver a transformar en la representación base.

También pensé que alguien debería mencionar también lo que debería hacer para evitar el corte ... Obtenga una copia de los Estándares de codificación de C ++, las pautas de las 101 reglas y las mejores prácticas. Tratar con rebanar es el # 54.

Sugiere un patrón un tanto sofisticado para abordar completamente el problema: tener un constructor de copia protegido, un DoClone virtual puro protegido y un Clon público con una afirmación que le dirá si una clase derivada (adicional) no implementó 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.


Está bien, lo intentaré después de leer muchas publicaciones que explican cómo se corta el objeto, 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 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 miembro virtual que accede al estado de divisió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()); }

La salida es:

Call by reference I am a ''B'' Call by copy ''A'' copy constructor I am an ''A''


La mayoría de las respuestas aquí no explican cuál es el problema real con el corte en rodajas. Solo explican los casos benignos de rebanar, no los traidores. Supongamos, como las otras respuestas, que se trata de dos clases A y B , donde B deriva (públicamente) de A

En esta situación, C ++ le permite pasar una instancia de B al operador de asignación de A (y también al constructor de copia). Esto funciona porque una instancia de B se puede convertir en 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;

Allí no pasa nada malo. Pediste una instancia de A que es una copia de B , y eso es exactamente lo que obtienes. Claro, a no contendrá algunos de los miembros de b , pero ¿cómo debería hacerlo? Después de todo, es una A , no una B , por lo que ni siquiera ha oído hablar de estos miembros, y mucho menos podría almacenarlos.

El caso traicionero

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

Se podría pensar que b2 será una copia de b1 después. Pero, ¡ay, no lo es! Si lo inspecciona, descubrirá 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 las funciones no virtuales, el tipo declarado (que es A& ) determina qué función se llama, a diferencia del tipo real (que sería B , dado que a_ref referencia a una instancia de B ). Ahora, el operador de asignación de A obviamente solo sabe acerca de los miembros declarados en A , por lo que solo copiará esos, dejando a los miembros agregados en B sin cambios.

Una solución

Asignar solo partes de un objeto por lo general no tiene mucho sentido, pero desafortunadamente C ++ no proporciona una forma integrada de prohibir esto. Usted puede, sin embargo, rodar el suyo propio. 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 realizar la asignación real en un miembro (protegido!) assign() , ya que la assign() B assign() probablemente querrá usar la assign() para copiar los miembros 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 } };

Tenga en cuenta que, para mayor comodidad, el operator= B operator= reemplaza de forma covariante el tipo de retorno, ya que sabe que está devolviendo una instancia de B


La tercera coincidencia en Google para "C ++ slicing" me ofrece 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 asignas un objeto de una subclase a la súper clase. 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.


Me parece que el corte no es un problema más que cuando sus propias clases y programas están mal diseñados / diseñados.

Si paso un objeto de subclase como un parámetro a un método, que toma un parámetro de tipo superclase, sin duda debería tenerlo en cuenta y saber el método interno, el método llamado solo funcionará con el objeto superclase (también conocido como clase base).

Me parece que solo la expectativa irrazonable de que proporcionar una subclase donde se solicita una clase de 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 subclases deficiente. Supongo que generalmente es el resultado de sacrificar un buen diseño de POO en favor de la conveniencia o la mejora del rendimiento.


Si tiene una clase base A y una clase derivada B , entonces 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 básica A

Por lo tanto, para llamar a wantAnA , el compilador " wantAnA " todos los miembros adicionales de la clase derivada. El resultado podría ser un objeto que no deseaba crear, porque

  • puede ser incompleto,
  • se comporta como un objeto A (todo el comportamiento especial de la clase B se pierde).

cuando un objeto de clase derivado se asigna a un objeto de clase base, los atributos adicionales de un objeto de clase derivado 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 }


1. LA DEFINICIÓN DE PROBLEMAS DE SLICING

Si D es una clase derivada de la clase base B, entonces 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 la asignación anterior está permitida, el valor asignado a la mascota variable pierde su campo de reproducción. Esto se llama el problema de rebanar .

2. COMO ARREGLAR EL PROBLEMA DE LICENCIAS

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 usar funciones, la función debe ser una función virtual.


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; }