¿Por qué shared_ptr <void> es legal, mientras que unique_ptr <void> está mal formado?

100

La pregunta realmente encaja en el título: tengo curiosidad por saber cuál es la razón técnica de esta diferencia, pero también la justificación.

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;
Anuncio N
fuente

Respuestas:

118

Es porque std::shared_ptrimplementa el borrado de tipos, mientras std::unique_ptrque no lo hace.


Dado que std::shared_ptrimplementa el borrado de tipos, también admite otra propiedad interesante, a saber. sí no necesita el tipo de la Deleter como argumento de tipo de plantilla a la plantilla de clase. Mira sus declaraciones:

template<class T,class Deleter = std::default_delete<T> > 
class unique_ptr;

que tiene Deletercomo parámetro de tipo, mientras que

template<class T> 
class shared_ptr;

no lo tiene.

Ahora la pregunta es, ¿por qué shared_ptrimplementa el borrado de tipo? Bueno, lo hace, porque tiene que admitir el recuento de referencias, y para admitir esto, tiene que asignar memoria desde el montón y, dado que tiene que asignar memoria de todos modos, va un paso más allá e implementa el borrado de tipos, que necesita montón asignación también. ¡Así que básicamente es ser oportunista!

Debido al borrado de tipo, std::shared_ptrpuede admitir dos cosas:

  • Puede almacenar objetos de cualquier tipo void*, pero aún puede eliminar los objetos en la destrucción correctamente invocando correctamente su destructor .
  • El tipo de eliminador no se pasa como argumento de tipo a la plantilla de clase, lo que significa un poco de libertad sin comprometer la seguridad de tipos .

Bien. Eso se trata de cómo std::shared_ptrfunciona.

Ahora la pregunta es, ¿se pueden std::unique_ptralmacenar objetos como void* ? Bueno, la respuesta es , siempre que pase un eliminador adecuado como argumento. Aquí hay una de esas demostraciones:

int main()
{
    auto deleter = [](void const * data ) {
        int const * p = static_cast<int const*>(data);
        std::cout << *p << " located at " << p <<  " is being deleted";
        delete p;
    };

    std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);

} //p will be deleted here, both p ;-)

Salida ( demostración en línea ):

959 located at 0x18aec20 is being deleted

Hiciste una pregunta muy interesante en el comentario:

En mi caso, necesitaré un eliminador de borrado de tipos, pero también parece posible (a costa de una asignación de montón). Básicamente, ¿significa esto que en realidad hay un lugar de nicho para un tercer tipo de puntero inteligente: un puntero inteligente de propiedad exclusiva con borrado de tipo?

a lo que @Steve Jessop sugirió la siguiente solución,

En realidad, nunca he intentado esto, pero tal vez podrías lograrlo usando un tipo apropiado std::functioncomo eliminador con unique_ptr? Suponiendo que realmente funciona, entonces ya está hecho, propiedad exclusiva y un eliminador de tipo borrado.

Siguiendo esta sugerencia, implementé esto (aunque no hace uso std::functionya que no parece necesario):

using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
    return unique_void_ptr(ptr, [](void const * data) {
         T const * p = static_cast<T const*>(data);
         std::cout << "{" << *p << "} located at [" << p <<  "] is being deleted.\n";
         delete p;
    });
}

int main()
{
    auto p1 = unique_void(new int(959));
    auto p2 = unique_void(new double(595.5));
    auto p3 = unique_void(new std::string("Hello World"));
}  

Salida ( demostración en línea ):

{Hello World} located at [0x2364c60] is being deleted.
{595.5} located at [0x2364c40] is being deleted.
{959} located at [0x2364c20] is being deleted.

Espero que ayude.

Nawaz
fuente
13
Buena respuesta, +1. Pero puede mejorarlo aún más si menciona explícitamente que std::unique_ptr<void, D>todavía es posible proporcionar un archivo D.
Angew ya no se enorgullece de SO
1
@Angrew: Buena, encontraste la verdadera pregunta subyacente que no estaba escrita en mi pregunta;)
Anuncio N
@Nawaz: Gracias. En mi caso, necesitaré un eliminador de borrado de tipos, pero también parece posible (a costa de una asignación de montón). Básicamente, ¿significa esto que en realidad hay un lugar de nicho para un tercer tipo de puntero inteligente: un puntero inteligente de propiedad exclusiva con borrado de tipo?
Anuncio N
8
@AdN: En realidad, nunca he intentado esto, pero tal vez podrías lograrlo usando un tipo apropiado std::functioncomo eliminador con unique_ptr? Suponiendo que realmente funciona, entonces ya está hecho, propiedad exclusiva y un eliminador de tipo borrado.
Steve Jessop
Gramática nit: "¿por qué X verbos Y?" debe ser "¿por qué X verbo Y?" en inglés.
zwol
7

Uno de los fundamentos está en uno de los muchos casos de uso de un shared_ptr, a saber, como indicador de por vida o centinela.

Esto se mencionó en la documentación original de boost:

auto register_callback(std::function<void()> closure, std::shared_ptr<void> pv)
{
    auto closure_target = { closure, std::weak_ptr<void>(pv) };
    ...
    // store the target somewhere, and later....
}

void call_closure(closure_target target)
{
    // test whether target of the closure still exists
    auto lock = target.sentinel.lock();
    if (lock) {
        // if so, call the closure
        target.closure();
    }
}

¿Dónde closure_targethay algo como esto?

struct closure_target {
    std::function<void()> closure;
    std::weak_ptr<void> sentinel;
};

La persona que llama registraría una devolución de llamada similar a esta:

struct active_object : std::enable_shared_from_this<active_object>
{
    void start() {
      event_emitter_.register_callback([this] { this->on_callback(); }, 
                                       shared_from_this());
    }

    void on_callback()
    {
        // this is only ever called if we still exist 
    }
};

debido a shared_ptr<X>que siempre es convertible a shared_ptr<void>, event_emitter ahora puede ser felizmente inconsciente del tipo de objeto al que está llamando.

Este arreglo libera a los suscriptores del emisor de eventos de la obligación de manejar los casos de cruce (¿qué pasa si la devolución de llamada está en una cola, esperando ser accionada mientras el objeto_activo desaparece?), Y también significa que no hay necesidad de sincronizar la cancelación de la suscripción. weak_ptr<void>::lockes una operación sincronizada.

Richard Hodges
fuente