Clases base abstractas y construcción de copias, reglas generales

10

Muchas veces es una buena idea tener una clase base abstracta para aislar la interfaz del objeto.

El problema es que la construcción de copia, en mi humilde opinión, está bastante rota por defecto en C ++, y los constructores de copia se generan de forma predeterminada.

Entonces, ¿cuáles son las trampas cuando tienes una clase base abstracta y punteros sin procesar en clases derivadas?

class IAbstract
{
    ~IAbstract() = 0;
}

class Derived : public IAbstract
{
    char *theProblem;
    ...
}

IAbstract *a1 = new Derived();
IAbstract a2 = *a1;//???

¿Y ahora deshabilita limpiamente la construcción de copia para toda la jerarquía? ¿Declarar copia de construcción como privada en IAbstract?

¿Hay alguna regla de tres con clases base abstractas?

Descifrador
fuente
1
use referencias en lugar de punteros :)
tp1
@ tp1: o algún puntero inteligente al menos.
Benjamin Bannier
1
A veces solo tienes que trabajar con el código existente ... No puedes cambiar todo en un instante.
Codificador
¿Por qué crees que el constructor de copia predeterminado está roto?
BЈовић
2
@Coder: la guía de estilo de Google es un montón de basura y es una mierda para cualquier desarrollo de C ++.
DeadMG

Respuestas:

6

La construcción de copias en una clase abstracta debe hacerse privada en la mayoría de los casos, así como el operador de asignación.

Las clases abstractas, por definición, están hechas para ser de tipo polimórfico. Por lo tanto, no sabe cuánta memoria está usando su instancia y, por lo tanto, no puede copiarla ni asignarla de manera segura. En la práctica, corre el riesgo de cortar: /programming/274626/what-is-the-slicing-problem-in-c

El tipo polimórfico, en C ++, no debe manipularse por valor. Los manipula por referencia o por puntero (o cualquier puntero inteligente).

Esta es la razón por la cual Java ha hecho que los objetos sean manipulables solo por referencia, y por qué C # y D tienen la separación entre clases y estructuras (la primera es polimórfica y tipo de referencia, la segunda es no polimórfica y tipo de valor).

deadalnix
fuente
2
Las cosas en Java no son mejores. En Java es doloroso copiar cualquier cosa, incluso cuando realmente lo necesita, y es muy fácil olvidar hacerlo. Entonces terminas con errores desagradables donde dos estructuras de datos tienen una referencia a la misma clase de valor (por ejemplo, Fecha), y luego una de ellas cambia la Fecha y la otra estructura de datos ahora está rota.
Kevin Cline
3
Meh No es un problema: cualquiera que conozca C ++ no cometerá este error. Más importante aún, no hay ninguna razón para manipular los tipos polimórficos por valor si sabes lo que estás haciendo. Estás iniciando una reacción total instintiva ante una posibilidad técnicamente escasa. Los punteros NULL son un problema mayor.
DeadMG
8

Por supuesto, podría protegerlo y vaciarlo, para que las clases derivadas puedan elegir. Sin embargo, de manera más general, su código está prohibido de todos modos porque es imposible crear una instancia IAbstract, ya que tiene una función virtual pura. Como tal, esto generalmente no es un problema: sus clases de interfaz no pueden instanciarse y, por lo tanto, nunca pueden copiarse, y sus clases más derivadas pueden prohibir o seguir copiando como lo deseen.

DeadMG
fuente
¿Y qué pasa si hay una jerarquía Resumen -> Derivada1 -> Derivada2 y Derivada2 está asignada a Derivada1? Estos rincones oscuros del lenguaje todavía me sorprenden.
Codificador
@Coder: Eso generalmente se ve como PEBKAC. Sin embargo, más directamente, siempre puede declarar un privado operator=(const Derived2&)en Derived1.
DeadMG
Ok, tal vez esto realmente no sea un problema. Solo quiero asegurarme de que la clase sea segura contra el abuso.
Codificador
2
Y para eso deberías obtener una medalla.
Ingeniero mundial
2
@Coder: ¿Abuso de quién? Lo mejor que puede hacer es facilitar la escritura del código correcto. No tiene sentido tratar de defenderse de los programadores que no conocen C ++.
Kevin Cline
2

Al hacer que ctor y la asignación sean privados (o al declararlos como = eliminar en C ++ 11), deshabilita la copia.

El punto aquí es DONDE tienes que hacer eso. Para permanecer con su código, IAbstract no es un problema. (tenga en cuenta que al hacer lo que hizo, asigna el *a1 IAbstractsubobjeto a a2, perdiendo cualquier referencia a Derived. La asignación de valor no es polimórfica)

El problema viene con Derived::theproblem. Copiar un Derivado en otro puede, de hecho, compartir los *theproblemdatos que pueden no estar diseñados para ser compartidos (hay dos instancias que pueden llamar delete theproblema su destructor).

Si ese es el caso, es Derivedque debe ser no copiable y no asignable. Por supuesto, si hace que la copia sea privada IAbstract, ya que la copia predeterminada la Derivednecesita, Derivedtampoco será copiable. Pero si define el suyo Derived::Derived(const Derived&)sin llamar a IAbtractcopy, aún puede copiarlos.

El problema está en Derived, y la solución debe permanecer en Derived: si debe ser un objeto dinámico solo al que se accede solo mediante punteros o referencias, es Derived en sí mismo el que debe tener

class Derived
{
    ...
    Derived(const Derived&) = delete;
    Derived& operator=(const Derived&) = delete;
};

Básicamente, depende del diseñador de la clase Derived (que debe saber cómo funciona Derived y cómo theproblemse gestiona) decidir qué hacer con la asignación y la copia.

Emilio Garavaglia
fuente