2010-05-04 17 views
8

Quiero realizar "copias en profundidad" de un contenedor STL de punteros a clases polimórficas.C++ Virtual Constructor, sin clon()

lo que sé sobre el patrón de diseñoPrototipo, implementado por medio de la virtual Ctor Idiom, como se explica en el C++ FAQ Lite, Item 20.8.
Es simple y directo:

struct ABC // Abstract Base Class 
{ 
    virtual ~ABC() {} 
    virtual ABC * clone() = 0; 
}; 
struct D1 : public ABC 
{ 
    virtual D1 * clone() { return new D1(*this); } // Covariant Return Type 
}; 

una copia profunda es entonces:

for(i = 0; i < oldVector.size(); ++i) 
    newVector.push_back(oldVector[i]->clone()); 

Inconvenientes

Como Andrei Alexandrescu states it:

La aplicación clone() debe seguir el mismo patrón en todos los clas derivados ses; a pesar de su estructura repetitiva, no hay una forma razonable de automatizar la definición de la función de miembro clone() (más allá de las macros, eso es).

Por otra parte, los clientes de ABC pueden posiblemente hacer algo malo. (Es decir, nada impide a los clientes a hacer algo malo, así, se suceder.)

mejor diseño?

Mi pregunta es: ¿hay alguna otra forma de hacer una clase base abstracta clonable sin requerir que las clases derivadas escriban código relacionado con el clon? (Clase auxiliar? Plantillas?)


siguiente es mi contexto. Con suerte, ayudará a entender mi pregunta.

Estoy diseñando una jerarquía de clases para realizar operaciones en una clase Image:

struct ImgOp 
{ 
    virtual ~ImgOp() {} 
    bool run(Image &) = 0; 
}; 

operaciones de imagen son definidos por el usuario: los clientes de la jerarquía de clases va a poner en práctica sus propias clases derivan de ImgOp:

struct CheckImageSize : public ImgOp 
{ 
    std::size_t w, h; 
    bool run(Image &i) { return w==i.width() && h==i.height(); } 
}; 
struct CheckImageResolution { ... }; 
struct RotateImage   { ... }; 
... 

operaciones múltiples pueden ser realizadas secuencialmente en una imagen:

bool do_operations(vector< ImgOp* > v, Image &i) 
{ 
    for_each(v.begin(), v.end(), 
     /* bind2nd(mem_fun(&ImgOp::run), i ...) don't remember syntax */); 
} 

Si hay varias imágenes, el conjunto se puede dividir y compartir en varios subprocesos. Para garantizar la seguridad del hilo, cada hilo debe tener su propia copia de todos los objetos de operación contenidos en v - v se convierte en un prototipo para ser copiado profundamente en cada hilo.

Editado: La versión flujos seguros utiliza el patrón de diseño de prototipos para hacer cumplir copia de punta-a-objetos - PAD: no

struct ImgOp 
{ 
    virtual ~ImgOp() {} 
    bool run(Image &) = 0; 
    virtual ImgOp * clone() = 0; // virtual ctor 
}; 

struct CheckImageSize : public ImgOp  { /* no clone code */ }; 
struct CheckImageResolution : public ImgOp { /* no clone code */ }; 
struct RotateImage : public ImgOp   { /* no clone code */ }; 

bool do_operations(vector< ImgOp* > v, Image &i) 
{ 
    // In another thread 
    vector< ImgOp* > v2; 
    transform(v.begin(), v.end(),      // Copy pointed-to- 
     back_inserter(v2), mem_fun(&ImgOp::clone)); // objects 
    for_each(v.begin(), v.end(), 
     /* bind2nd(mem_fun(&ImgOp::run), i ...) don't remember syntax */); 
} 

Esto tiene sentido cuando las clases de operación de imagen son pequeños : no serialice accesos a instancias únicas de ImgOp s, en lugar de proporcionar a cada subproceso sus propias copias.

La parte difícil es evitar que los escritores de las nuevas ImgOp -derivadas clases escriban cualquier código relacionado con el clon. (Debido a que este es el detalle de la implementación, esta es la razón por la que descarté las respuestas de Paul con el patrón curiosamente recurrente).

+1

Yo diría que, de hecho, es posible automatizar 'clone()' (si es un poco poco elegante): http://nerdland.net/2009/06/covariant-templatized-virtual-copy-constructors/ –

+1

@tyler McHenry : Pero hacerlo automático alentaría el mal uso y luego C++ comenzaría a parecerse a Java. Yo diría que casi todos los usos de clone() son incorrectos (en los pocos casos en los que es necesario permitir que la gente haga el trabajo de implementarlo) desafortunadamente es fácil de usar de manera incorrecta lo que lleva a un mal diseño e implementación. –

+0

¿Qué hay de malo con solo definir y usar el constructor de copia de la manera en que debería hacerlo? Como dice @Martin, definitivamente no quieres que los objetos sean clonables arbitrariamente. – jalf

Respuesta

2

FYI, este es el diseño con el que salí. Gracias Paul y FredOverflow por sus entradas. (. Y Martin York por su comentario)

Paso # 1, tiempo de compilación polimorfismo con plantillas

polimorfismo se realiza en tiempo de compilación utilizando plantillas y implícitas interfaces:

template< typename T > 
class ImgOp 
{ 
    T m_t; // Not a ptr: when ImgOp is copied, copy ctor and 
      // assignement operator perform a *real* copy of object 
    ImageOp (const ImageOp &other) : m_t(other .m_t) {} 
    ImageOp & operator=(const ImageOp &); 
public: 
    ImageOp (const T &p_t) : m_t(p_t) {} 
    ImageOp<T> * clone() const { return new ImageOp<T>(*this); } 
    bool run(Image &i) const { return m_t.run(i); } 
}; 

// Image operations need not to derive from a base class: they must provide 
// a compatible interface 
class CheckImageSize  { bool run(Image &i) const {...} }; 
class CheckImageResolution { bool run(Image &i) const {...} }; 
class RotateImage   { bool run(Image &i) const {...} }; 

Ahora todo el código relacionado con el clon se encuentra dentro de una clase única.Sin embargo, ahora es imposible tener un contenedor de ImgOp s a plantillas en diferentes operaciones:

vector<ImgOp> v;   // Compile error, ImgOp is not a type 
vector< ImgOp<ImgOp1> > v; // Only one type of operation :/ 

Paso # 2, añadir un nivel de abstracción

Añadir una base de plantilla no actúa como una interfaz:

class AbstractImgOp 
{ 
    ImageOp<T> * clone() const = 0; 
    bool run(Image &i) const = 0; 
}; 

template< typename T > 
class ImgOp : public AbstractImgOp 
{ 
    // No modification, especially on the clone() method thanks to 
    // the Covariant Return Type mechanism 
}; 

Ahora podemos escribir:

vector< AbstractImgOp* > v; 

pero se hace difícil de manipular objetos de operación imagen:

AbstractImgOp *op1 = new AbstractImgOp; 
    op1->w = ...; // Compile error, AbstractImgOp does not have 
    op2->h = ...; // member named 'w' or 'h' 

CheckImageSize *op1 = new CheckImageSize; 
    op1->w = ...; // Fine 
    op1->h = ...; 
AbstractImgOp *op1Ptr = op1; // Compile error, CheckImageSize does not derive 
          // from AbstractImgOp? Confusing 

CheckImageSize op1; 
    op1.w = ...; // Fine 
    op1.h = ...; 
CheckImageResolution op2; 
    // ... 
v.push_back(new ImgOp<CheckImageSize>(op1));  // Confusing! 
v.push_back(new ImgOp<CheckImageResolution>(op2)); // Argh 

Paso # 3, agregar un "puntero clonación" clase

Sobre la base de la solución de la FredOverflow, hacer que un puntero de clonación para hacer el marco más sencillo de usar.
Sin embargo, este puntero no tiene que ser a plantillas para los que está diseñado para contener un solo tipo de PTR - sólo el ctor tiene que ser a plantillas:

class ImgOpCloner 
{ 
    AbstractImgOp *ptr; // Ptr is mandatory to achieve polymorphic behavior 
    ImgOpCloner & operator=(const ImgOpCloner &); 
public: 
    template< typename T > 
    ImgOpCloner(const T &t) : ptr(new ImgOp<T>(t)) {} 
    ImgOpCloner(const AbstractImgOp &other) : ptr(other.ptr->clone()) {} 
    ~ImgOpCloner() { delete ptr; } 
    AbstractImgOp * operator->() { return ptr; } 
    AbstractImgOp & operator*() { return *ptr; } 
}; 

Ahora podemos escribir:

CheckImageSize op1; 
    op1.w = ...; // Fine 
    op1.h = ...; 
CheckImageResolution op2; 
    // ... 
vector<ImgOpCloner> v; 
v.push_back(ImgOpCloner(op1)); // This looks like a smart-ptr, this is not 
v.push_back(ImgOpCloner(op2)); // confusing anymore -- and intent is clear 
7

Puede utilizar el patrón curiosamente recursivo pero puede hacer que su código sea menos legible. Aún necesitará constructores de copias. Funciona de la siguiente manera.

struct ABC // Abstract Base Class 
{ 
    virtual ~ABC() {} 
    virtual ABC * clone() const = 0; 
}; 



template <class TCopyableClass> 
struct ClonableABC : public ABC 
{ 
    virtual ABC* clone() const { 
     return new TCopyableClass(*(TCopyableClass*)this); 
    } 
}; 


struct SomeABCImpl : public ClonableABC<SomeABCImpl> 
{}; 
+0

+1: Siempre interesado en una aplicación de patrón curiosamente recursiva ... – neuro

+2

+1 buen enfoque. Como se señaló en el artículo que Tyler mencionó en un comentario, ya no tiene un tipo de devolución covariante. Sin embargo, nunca entendí por qué era tan importante. Los lugares que llamaría un método de clonación, no sé el tipo derivado de todos modos. – Dan

+0

Según tengo entendido, esto no funcionará si necesito crear la subclase 'SomeABCImpl', ¿verdad? – doublep

1

una copia profunda es entonces: [bucle]

a sacar el clon cliente el vector de forma explícita. No estoy seguro de si esto responde su pregunta, pero sugeriría un vector de punteros inteligentes para que la clonación ocurra automáticamente.

std::vector<cloning_pointer<Base> > vec; 
vec.push_back(cloning_pointer<Base>(new Derived())); 

// objects are automatically cloned: 
std::vector<cloning_pointer<Base> > vec2 = vec; 

Por supuesto, no quieren estas copias implícitos que sucedan al cambiar el tamaño de un vector o algo así, por lo que necesita para ser capaz de distinguir las copias de movimientos. Aquí está mi implementación de juguetes C++ 0x de cloning_pointer que tal vez tenga que ajustar a sus necesidades.

#include <algorithm> 

template<class T> 
class cloning_pointer 
{ 
    T* p; 

public: 

    explicit cloning_pointer(T* p) 
    { 
     this->p = p; 
    } 

    ~cloning_pointer() 
    { 
     delete p; 
    } 

    cloning_pointer(const cloning_pointer& that) 
    { 
     p = that->clone(); 
    } 

    cloning_pointer(cloning_pointer&& that) 
    { 
     p = that.p; 
     that.p = 0; 
    } 

    cloning_pointer& operator=(const cloning_pointer& that) 
    { 
     T* q = that->clone(); 
     delete p; 
     p = q; 
     return *this; 
    } 

    cloning_pointer& operator=(cloning_pointer&& that) 
    { 
     std::swap(p, that.p); 
     return *this; 
    } 

    T* operator->() const 
    { 
     return p; 
    } 

    T& operator*() const 
    { 
     return *p; 
    } 
}; 

Julien: && no es un "ref de ref", es una referencia rvalue que sólo se une a rvalues ​​modificables. Consulte este excelente (pero tristemente anticuado) tutorial y video para obtener una descripción general de las referencias de rvalue y cómo funcionan.

+0

Esto es interesante. Por favor, ¿pueden dar más detalles sobre el mecanismo "en movimiento"? ¿Cómo sabe el clonador_poder que debe moverse y no clonarse? Cada vez que se le da un ref de ref, ¿significa que está siendo redimensionado? Por cierto, no debería usar 'std :: swap()' pero 'usando std :: swap; intercambiar(). –

+0

@Julien Sé sobre la expresión de intercambio, pero creo que es bastante seguro intercambiar dos punteros crudos con 'std :: swap' ;-) Incluí dos enlaces para usted que explican bastante bien el mecanismo de movimiento. – fredoverflow

+0

Muchas gracias por los detalles (y por la corrección de intercambio :) Me gusta la clase de ayuda 'cloning_pointer' porque hace que el intento sea claro. Sin embargo, no resuelve mi problema porque las clases derivadas todavía tienen que implementar el método 'clone()'. –