Puedo tener polimórficos contenedores con el valor de la semántica en C++?

StackOverflow https://stackoverflow.com/questions/41045

  •  09-06-2019
  •  | 
  •  

Pregunta

Como regla general, yo prefiero usar un valor en lugar de puntero de la semántica en C++ (es decir, utilizando vector<Class> en lugar de vector<Class*>).Generalmente la ligera pérdida de rendimiento es más que compensó por no tener que recordar para eliminar los objetos asignados dinámicamente.

Por desgracia, el valor de las colecciones no funcionan cuando se desea almacenar una gran variedad de tipos de objeto que todas derivan de una base común.Vea el ejemplo de abajo.

#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 mi pastel (valor semántica) y comer demasiado (polimórficos contenedores)?O tengo que usar punteros?

¿Fue útil?

Solución

Puesto que los objetos de diferentes clases tienen diferentes tamaños, terminará ejecutando en la segmentación problema si se almacenan como valores.

Una solución razonable es la de almacenar contenedor seguro de punteros inteligentes.Yo normalmente uso boost::shared_ptr que es seguro 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 usa el conteo de referencias para que no se elimine la instancia subyacente hasta que todas las referencias son eliminados.

Otros consejos

Sí, se puede.

El boost.ptr_container biblioteca proporciona polimórficos valor semántico de las versiones de los contenedores estándar.Usted sólo tiene que pasar un puntero a un montón asignados por el objeto, y el contenedor deberá tomar posesión y todas las otras operaciones proporcionará el valor de la semántica , excepto para la recuperación de la propiedad, lo que le da casi todos los beneficios de valor semántica mediante el uso de un puntero inteligente.

Yo sólo quería señalar que el vector<Foo> es generalmente más eficiente que el vector<Foo*>.En un vector<Foo>, de todas las Foos será uno al lado del otro en la memoria.Suponiendo un frío TLB y la memoria caché, la primera lectura se añade la página a la TLB y tire un pedazo del vector en el L# cachés;las lecturas posteriores hará uso de la tibia y la caché de la carga TLB, con ocasionales errores de caché y menos frecuentes fallos de TLB.

Contraste esto con un vector<Foo*>:Como rellenar el vector, se puede obtener Foo*'s desde el asignador de memoria.Asumiendo que su asignación no es muy inteligente, (tcmalloc?) o rellenar el vector lentamente con el tiempo, la ubicación de cada uno de los Foo es probable que sean mucho aparte de los otros Foos:tal vez sólo por cientos de bytes, tal vez megabytes aparte.

En el peor de los casos, a medida que se exploran a través de un vector<Foo*> y eliminar la referencia de cada indicador se incurrirá en un fallo de TLB y la caché -- esto va a terminar siendo un mucho más lento que si había un vector<Foo>.(Bueno, en realidad peor de los casos, cada uno de los Foo se ha paginado en el disco, y cada lectura incurre en un disco buscar() y read() para mover la página en la memoria.)

Así, se puede seguir utilizando el vector de<Foo> siempre que sea apropiado.:-)

La mayoría de los tipos de contenedor quiere resumen el particular, la estrategia de almacenamiento, se ha vinculado lista, vector, árbol o lo que sea.Por esta razón, usted va a tener problemas con la posesión y el consumo de la mencionada pastel (es decir, el pastel es mentira (NB:alguien tenía que hacer esta broma)).

Entonces, ¿qué hacer?Bueno, hay un par de opciones de cute, pero la mayoría se reducen a las variantes en uno de los pocos temas o combinaciones de ellas:la recolección o la invención de un adecuado puntero inteligente, jugando con las plantillas plantilla o plantillas en cierta manera inteligente, utilizando una interfaz común para containees que proporciona un gancho para la implementación de los per-containee doble-dispatch.

Hay tensión básica entre los dos objetivos, por lo que debe decidir lo que quiere, entonces tratamos de diseñar algo que se consigue básicamente lo que desea.Es es es posible hacer algo bonito e inesperado de los trucos para conseguir punteros a mirar como valores lo suficientemente inteligente como para el recuento de referencias y lo suficientemente inteligente como para implementaciones de una fábrica.La idea básica es utilizar el recuento de referencias y de la copia en la demanda y constness y (para el factor) una combinación del preprocesador, plantillas, y C++'s inicialización estática reglas para obtener algo que es tan inteligente como sea posible acerca de la automatización de puntero de conversiones.

Tengo, en el pasado, pasó algún tiempo tratando de imaginar cómo utilizar Virtual de Proxy / Sobre-Carta / que lindo truco con referencia contado punteros para lograr algo como una base para el valor semántico de programación en C++.

Y creo que se podría hacer, pero usted tendrá que proporcionar un relativamente cerrado, C#-código administrado mundo en C++ (aunque uno de los que puede romper a través de a de C++ subyacente cuando sea necesario).Así que tengo un montón de simpatía para su línea de pensamiento.

Usted también podría considerar la posibilidad boost::cualquier.Lo he utilizado para heterogéneo de contenedores.Cuando se lee el valor de la espalda, es necesario realizar un any_cast.Lanzará una bad_any_cast si se produce un error.Si eso sucede, usted puede coger y pasar al siguiente tipo.

Yo creo lanzará una bad_any_cast si intenta any_cast una clase derivada a su base.Lo he probado:

  // 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

Todo lo que se dijo, también me gustaría ir a la shared_ptr ruta de primera!Sólo pensé que esto podría ser de algún interés.

Echa un vistazo a static_cast y reinterpret_cast
En el Lenguaje de Programación C++, 3ª ed, Bjarne Stroustrup describe en la página 130.Hay una sección entera sobre esto en el Capítulo 6.
Usted puede refundición de su clase Padre del Niño de la clase.Esto requiere que usted sepa cuando cada uno es cada cual.En el libro, el Dr.Stroustrup habla acerca de las diferentes técnicas para evitar esta situación.

No hacer esto.Esto hace que el polimorfismo de que usted está tratando de lograr en el primer lugar!

Sólo añadir una cosa a todos 1800 INFORMACIÓN ya se dijo.

Es posible que desee echar un vistazo a "Más Eficaz De C++" por Scott Mayers "Punto 3:Nunca tratar de matrices polymorphically" en el fin de comprender mejor este problema.

Estoy usando mi propia colección con plantilla de clase con exposición del tipo de valor de la semántica, pero internamente se almacena punteros.Es el uso de una costumbre clase iterator que cuando se eliminan las referencias obtiene un valor de referencia en lugar de un puntero.La copia de la colección hace profundo elemento de copias, en lugar de la duplicación de los punteros, y aquí es donde la mayoría de la sobrecarga de mentiras (realmente una cuestión menor, se considera lo puedo conseguir en su lugar).

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

Mientras que la búsqueda de una respuesta a este problema, me encontré con ésta y una pregunta similar.En la respuesta a la otra pregunta, usted encontrará dos soluciones:

  1. Usar std::opcional o boost::opcional y un visitante patrón.Esta solución hace que sea difícil para agregar nuevos tipos, pero fácil de añadir nueva funcionalidad.
  2. Utilizar una clase de contenedor similar a lo que Sean los Padres presenta en su charla.Esta solución hace que sea difícil para agregar nueva funcionalidad, pero fácil de añadir nuevos tipos.

El contenedor define la interfaz que usted necesita para sus clases y contiene un puntero a un objeto de este tipo.La implementación de la interfaz se realiza con funciones libres.

Aquí es un ejemplo de la implementación de este patrón:

class Shape
{
public:
    template<typename T>
    Shape(T t)
        : container(std::make_shared<Model<T>>(std::move(t)))
    {}

    friend void draw(const Shape &shape)
    {
        shape.container->drawImpl();
    }
    // add more functions similar to draw() here if you wish
    // remember also to add a wrapper in the Concept and Model below

private:
    struct Concept
    {
        virtual ~Concept() = default;
        virtual void drawImpl() const = 0;
    };

    template<typename T>
    struct Model : public Concept
    {
        Model(T x) : m_data(move(x)) { }
        void drawImpl() const override
        {
            draw(m_data);
        }
        T m_data;
    };

    std::shared_ptr<const Concept> container;
};

Las diferentes formas se implementan como regular las estructuras y clases.Usted es libre de elegir si desea utilizar las funciones miembro o función gratuita (pero usted tendrá que actualizar la anterior aplicación para utilizar las funciones miembro).Yo prefiero libre funciones:

struct Circle
{
    const double radius = 4.0;
};

struct Rectangle
{
    const double width = 2.0;
    const double height = 3.0;
};

void draw(const Circle &circle)
{
    cout << "Drew circle with radius " << circle.radius << endl;
}

void draw(const Rectangle &rectangle)
{
    cout << "Drew rectangle with width " << rectangle.width << endl;
}

Ahora usted puede agregar tanto Circle y Rectangle los objetos a la misma std::vector<Shape>:

int main() {
    std::vector<Shape> shapes;
    shapes.emplace_back(Circle());
    shapes.emplace_back(Rectangle());
    for (const auto &shape : shapes) {
        draw(shape);
    }
    return 0;
}

La desventaja de este modelo es que requiere una gran cantidad de repetitivo en la interfaz, ya que cada función debe estar definida en tres ocasiones.La ventaja es que usted obtenga copia-semántica:

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}

Esto produce:

Drew rectangle with width 2
Drew rectangle with width 2

Si usted está preocupado acerca de la shared_ptr, se puede reemplazar con un unique_ptr.Sin embargo, no será más copiable y usted tendrá que mover todos los objetos o implementar copiar manualmente.Sean los Padres se analiza en detalle en su charla y una aplicación se muestra en el mencionado respuesta.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top