Encontré el truco de shared_ptr para imitar el comportamiento de los destructores virtuales en un video de Youtube (https://www.youtube.com/watch?v=ZiNGWHg5Z-o&list=PLE28375D4AC946CC3&index=6), y al buscar en Internet encontré esta SO respuesta: shared_ptr magic :)

Normalmente, si B hereda de A y tiene su propio destructor, necesitamos un destructor virtual en la clase base A para garantizar que se llame correctamente al destructor de B. Sin embargo, con shared_ptr puede evitar la necesidad de un destructor virtual.

Como hay una sobrecarga de tiempo de ejecución de las búsquedas de vtable en funciones polimórficas, sentí curiosidad por saber si el truco shared_ptr puede evitar esta sobrecarga.

1
Anton 25 abr. 2017 a las 14:40

2 respuestas

La mejor respuesta

En primer lugar, el "truco" es posible con unique_ptr. Simplemente tiene que proporcionar un eliminador de borrado de texto, como se muestra a continuación.

Sin embargo, si observa la implementación, por supuesto verá que implica una llamada a través de un puntero de función, que es exactamente lo que hace un destructor virtual debajo de las cubiertas.

Las llamadas a funciones virtuales no son caras. Simplemente implican una búsqueda más de memoria. Si está haciendo esto en un ciclo cerrado, que es el único momento en que el rendimiento es un problema, es casi seguro que la recuperación se almacenará en caché.

Además, si el compilador puede demostrar que conoce el destructor correcto, eludirá por completo la búsqueda polimórfica (con optimizaciones activadas, por supuesto).

Para abreviar una larga historia, si esta es su única preocupación de rendimiento, entonces no tiene preocupaciones de rendimiento. Si tiene problemas de rendimiento y cree que se debe a los destructores virtuales, entonces, con respeto, ciertamente está equivocado.

Código de ejemplo:

#include <iostream>
#include <memory>

struct A {
    ~A() { std::cout << "~A\n"; }
};

struct B : A {
    ~B() { std::cout << "~B\n"; }
};


struct poly_deleter {
    template<class T>
    struct tag {
    };

    template<class T>
    static void delete_it(void *p) { delete reinterpret_cast<T *>(p); }

    template<class T>
    poly_deleter(tag<T>) : deleter_(&delete_it<T>) {}

    void operator()(void *p) const { deleter_(p); }

    void (*deleter_)(void *) = nullptr;
};

template<class T> using unique_poly_ptr = std::unique_ptr<T, poly_deleter>;

template<class T, class...Args> auto make_unique_poly(Args&&...args) -> unique_poly_ptr<T>
{
    return unique_poly_ptr<T> {
            new T (std::forward<Args>(args)...),
            poly_deleter(poly_deleter::tag<T>())
    };
};

int main()
{

    auto pb = make_unique_poly<B>();

    auto pa = std::move(pb);

    pa.reset();
}

Producción prevista:

~B
~A
3
Richard Hodges 25 abr. 2017 a las 12:39

El tipo de acciones ptr borra el destructor; varios tipos de borrado tipo tienden a tener costos "similares"; da o toma un factor de dos. Algunos guardan la memoria caché o dos sobre otros. Un tipo de borrado de tipo son las tablas de funciones virtuales.

Cómo exactamente el ptr compartido borra la destrucción se deja a la implementación. Pero el borrado de tipo nunca es completamente gratuito en comparación con una llamada de función en línea.

Será difícil demostrar cuál es más eficiente, ya que es probable que el agotamiento de la memoria caché sea más importante que cualquier microbenchmark que intente. Es poco probable que sea una gran fuente de desaceleración en cualquier caso.

1
Yakk - Adam Nevraumont 25 abr. 2017 a las 12:49