2008-09-03 7 views
30

Como regla general, prefiero usar valor en lugar de semántica de puntero en C++ (es decir, usando vector<Class> en lugar de vector<Class*>). Por lo general, la ligera pérdida de rendimiento se compensa con la eliminación de objetos dinámicamente asignados.¿Puedo tener contenedores polimórficos con semántica de valores en C++?

Desafortunadamente, las colecciones de valores no funcionan cuando desea almacenar una variedad de tipos de objetos que todos derivan de una base común. Vea el ejemplo a continuación.

#include <iostream> 

using namespace std; 

class Parent 
{ 
    public: 
     Parent() : parent_mem(1) {} 
     virtual void write() { cout << "Parent: " << parent_mem << endl; } 
     int parent_mem; 
}; 

class Child : public Parent 
{ 
    public: 
     Child() : child_mem(2) { parent_mem = 2; } 
     void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; } 

     int child_mem; 
}; 

int main(int, char**) 
{ 
    // I can have a polymorphic container with pointer semantics 
    vector<Parent*> pointerVec; 

    pointerVec.push_back(new Parent()); 
    pointerVec.push_back(new Child()); 

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output: 
    // 
    // Parent: 1 
    // Child: 2, 2 

    // But I can't do it with value semantics 

    vector<Parent> valueVec; 

    valueVec.push_back(Parent()); 
    valueVec.push_back(Child()); // gets turned into a Parent object :(

    valueVec[0].write();  
    valueVec[1].write();  

    // Output: 
    // 
    // Parent: 1 
    // Parent: 2 

} 

Mi pregunta es: ¿Puedo tener tener mi torta (la semántica de valor) y comérselo también (contenedores polimórficos)? ¿O tengo que usar punteros?

Respuesta

22

Dado que los objetos de las diferentes clases tendrán diferentes tamaños, usted terminaría corriendo en el problema de corte si los almacena como valores.

Una solución razonable es almacenar punteros inteligentes de contenedor seguro. Normalmente uso boost :: shared_ptr que es seguro de almacenar en un contenedor. Tenga en cuenta que std :: auto_ptr no lo es.

vector<shared_ptr<Parent>> vec; 
vec.push_back(shared_ptr<Parent>(new Child())); 

shared_ptr utiliza el recuento de referencias para que no se elimine la instancia subyacente hasta que se eliminen todas las referencias.

+3

'boost :: ptr_vector' es a menudo una alternativa más barata y sencilla a' std :: vector > ' – ben

+4

Esta respuesta no aborda la semántica del valor. shared_ptr proporciona semántica de referencia sobre las clases derivadas de T, para instace, shared_ptr a, b; b.reset (nuevo Derived1); a = b; no hace una copia del objeto Derived1. – Aaron

+5

Nunca dije que mi solución abordara la semántica de valores. Dije que era "una solución razonable". Si conoce una forma de tener una semántica de valores polimórficos, suba y obtenga su premio nobel. –

2

Tome un vistazo a static_cast y reinterpret_cast
En C++ Programming Language, 3ª ed, Bjarne Stroustrup describe en la página 130. Hay una sección completa sobre esto en el capítulo 6.
Usted puede refundir su clase de Padres a clase de Niños. Esto requiere que sepas cuando cada uno es cual. En el libro, el Dr. Stroustrup habla sobre diferentes técnicas para evitar esta situación.

No haga esto. ¡Esto niega el polimorfismo que estás tratando de lograr en primer lugar!

3

También podrías tener en cuenta boost::any. Lo he usado para contenedores heterogéneos. Cuando vuelva a leer el valor, debe realizar any_cast. Lanzará un bad_any_cast si falla. Si eso sucede, puede atrapar y pasar al siguiente tipo.

I cree arrojará un bad_any_cast si intenta any_cast una clase derivada a su base. Lo probé:

// But you sort of can do it with boost::any. 

    vector<any> valueVec; 

    valueVec.push_back(any(Parent())); 
    valueVec.push_back(any(Child()));  // remains a Child, wrapped in an Any. 

    Parent p = any_cast<Parent>(valueVec[0]); 
    Child c = any_cast<Child>(valueVec[1]); 
    p.write(); 
    c.write(); 

    // Output: 
    // 
    // Parent: 1 
    // Child: 2, 2 

    // Now try casting the child as a parent. 
    try { 
     Parent p2 = any_cast<Parent>(valueVec[1]); 
     p2.write(); 
    } 
    catch (const boost::bad_any_cast &e) 
    { 
     cout << e.what() << endl; 
    } 

    // Output: 
    // boost::bad_any_cast: failed conversion using boost::any_cast 

dicho todo esto, también me gustaría ir a la ruta shared_ptr primero! Solo pensé que esto podría ser de algún interés.

3

La mayoría de los tipos de contenedores desea abstraer la estrategia de almacenamiento particular, ya sea lista vinculada, vector, basada en árbol o lo que sea. Por esta razón, vas a tener problemas con la posesión y el consumo de la torta mencionada (es decir, la torta es mentira (NB: alguien tuvo que hacer esta broma)).

¿Qué hacer?Bueno, hay algunas opciones lindas, pero la mayoría se reducirá a variantes en uno de los pocos temas o combinaciones de ellos: escoger o inventar un puntero inteligente adecuado, jugar con plantillas o plantillas de plantillas de una manera inteligente, utilizando una interfaz común para containees que proporciona un gancho para implementar el doble despacho por contenedor.

Hay una tensión básica entre sus dos objetivos declarados, por lo que debe decidir lo que quiere, luego intente diseñar algo que le consiga básicamente lo que desea. Es es posible hacer algunos trucos agradables e inesperados para que los punteros se vean como valores con conteo de referencias lo suficientemente inteligente y las implementaciones suficientemente ingeniosas de una fábrica. La idea básica es usar el recuento de referencias y copiar a pedido y constness y (para el factor) una combinación del preprocesador, las plantillas y las reglas de inicialización estática de C++ para obtener algo lo más inteligente posible sobre la automatización de las conversiones del puntero.

En el pasado, he pasado tiempo tratando de imaginar cómo usar Virtual Proxy/Envelope-Letter/ese lindo truco con punteros contados de referencia para lograr algo así como una base para la programación semántica de valores en C++.

Y creo que se podría hacer, pero tendrías que proporcionar un mundo bastante cerrado, con código C# administrado dentro de C++ (aunque uno del que podrías acceder a C++ subyacente cuando sea necesario). Así que tengo mucha simpatía por su línea de pensamiento.

2

Solo para agregar una cosa a todos 1800 INFORMATION ya dicho.

Es posible que desee echar un vistazo a "More Effective C++" por Scott Mayers "Artículo 3: Nunca trate las matrices de forma polimórfica" con el fin de comprender mejor este problema.

10

Sí, puedes.

La biblioteca boost.ptr_container proporciona versiones semánticas de valores polimórficos de los contenedores estándar. Solo debe pasar un puntero a un objeto asignado en el montón, y el contenedor tomará posesión y todas las demás operaciones proporcionarán semántica de valores, excepto para recuperar la propiedad, lo que le brinda casi todos los beneficios de la semántica de valores mediante el uso de un puntero inteligente .

10

Solo quería señalar que el vector <Foo> suele ser más eficiente que el vector < Foo * >. En un vector <Foo>, todos los Foos estarán uno junto al otro en la memoria. Suponiendo un TLB y un caché fríos, la primera lectura agregará la página al TLB y extraerá un fragmento del vector en las cachés L #; las lecturas posteriores utilizarán la memoria caché activa y la TLB cargada, con errores ocasionales de caché y fallas TLB menos frecuentes.

Contraste esto con un vector < Foo * >: A medida que llena el vector, obtiene Foo * de su asignador de memoria. Asumiendo que su asignador no es extremadamente inteligente (¿tcmalloc?) O que llena el vector lentamente con el tiempo, la ubicación de cada Foo probablemente esté muy alejada de los otros Foos: tal vez solo por cientos de bytes, tal vez megabits separados.

En el peor de los casos, a medida que escanea a través de un vector < Foo * > y eliminación de referencias a cada puntero se incurrirá en una falta y errores de caché TLB - esto va a terminar siendo mucho más lento que si tuviera un vector <Foo>. (Bueno, en el peor de los casos, cada Foo ha sido enviado a un disco, y cada lectura tiene un disco seek() y read() para mover la página nuevamente a la RAM.)

Por lo tanto, siga usando el vector <Foo> cuando corresponda. :-)

+2

+1 para la consideración de la memoria caché Este problema será mucho más relevante en el futuro. –

+2

Depende de qué tan caro sea Foo :: Foo (const Foo &), ya que el contenedor de semántica de valores deberá llamarlo en insertos. – AndrewR

+0

Buen punto, sin embargo, siempre y cuando se agregue, no es un problema. (Tendrá log2 (n) copias adicionales.) Además, la mayoría de los accesos son lecturas y no escrituras; a veces sufrir la escritura costosa ocasional puede ser una ganancia neta al hacer las lecturas más rápido. – 0124816

1

Estoy usando mi propia clase de colección con plantillas con semántica de tipo de valor expuesto, pero internamente almacena punteros. Está utilizando una clase de iterador personalizada que cuando se desreferencia obtiene una referencia de valor en lugar de un puntero. Al copiar la colección, se hacen copias profundas de los elementos, en lugar de punteros duplicados, y es aquí donde recae la mayoría de los gastos indirectos (un problema realmente menor, considerado como el que recibo en su lugar).

Esa es una idea que podría satisfacer sus necesidades.

Cuestiones relacionadas