weak_ptr smart shared_ptr pointer example create c++ c++11 shared-ptr

c++ - smart - unique pointer



Diferencia en make_shared y normal shared_ptr en C++ (7)

std::shared_ptr<Object> p1 = std::make_shared<Object>("foo"); std::shared_ptr<Object> p2(new Object("foo"));

Hay muchas publicaciones de google y stackoverflow sobre esto, pero no puedo entender por qué make_shared es más eficiente que el uso directo de shared_ptr .

¿Puede alguien explicarme paso a paso la secuencia de los objetos creados y las operaciones realizadas por ambos para que pueda entender cómo make_shared es eficiente? He dado un ejemplo anterior para referencia.


El puntero compartido gestiona tanto el objeto en sí como un objeto pequeño que contiene el recuento de referencia y otros datos de mantenimiento. make_shared puede asignar un solo bloque de memoria para mantener ambos; la construcción de un puntero compartido de un puntero a un objeto ya asignado deberá asignar un segundo bloque para almacenar el recuento de referencia.

Además de esta eficiencia, el uso de make_shared significa que no necesita lidiar con punteros new y sin procesar, lo que ofrece una mejor seguridad de excepción: no hay posibilidad de lanzar una excepción después de asignar el objeto, pero antes de asignarlo al puntero inteligente .


Existe otro caso en el que las dos posibilidades difieren, además de las ya mencionadas: si necesita llamar a un constructor no público (protegido o privado), make_shared podría no tener acceso a él, mientras que la variante con el nuevo funciona bien .

class A { public: A(): val(0){} std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); } // Invalid because make_shared needs to call A(int) **internally** std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); } // Works fine because A(int) is called explicitly private: int val; A(int v): val(v){} };


La diferencia es que std::make_shared realiza una asignación de pila, mientras que llamar al constructor std::shared_ptr realiza dos.

¿Dónde ocurren las asignaciones de pila?

std::shared_ptr gestiona dos entidades:

  • el bloque de control (almacena metadatos como recuentos de ref, borrado de tipos, etc.)
  • el objeto que se logró

std::make_shared lleva a cabo una única asignación de std::make_shared espacio para el espacio necesario tanto para el bloque de control como para los datos. En el otro caso, el new Obj("foo") invoca una asignación de pila para los datos administrados y el constructor std::shared_ptr realiza otra para el bloque de control.

Para más información, consulte las notas de implementación en cppreference .

Actualización I: Excepción-Seguridad

Ya que los OP parecen estar preguntándose por el lado excepcional de la seguridad, he actualizado mi respuesta.

Considere este ejemplo,

void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ } F(std::shared_ptr<Lhs>(new Lhs("foo")), std::shared_ptr<Rhs>(new Rhs("bar")));

Debido a que C ++ permite un orden arbitrario de evaluación de subexpresiones, un orden posible es:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr<Lhs>
  4. std::shared_ptr<Rhs>

Ahora, supongamos que se Rhs una excepción en el paso 2 (por ejemplo, fuera de la excepción de memoria, el constructor de Rhs lanzó alguna excepción). Luego perdemos la memoria asignada en el paso 1, ya que nada habrá tenido la oportunidad de limpiarlo. El núcleo del problema aquí es que el puntero en bruto no se pasó al constructor std::shared_ptr inmediatamente.

Una forma de solucionar este problema es hacerlo en líneas separadas para que no pueda ocurrir este ordenamiento arbitrario.

auto lhs = std::shared_ptr<Lhs>(new Lhs("foo")); auto rhs = std::shared_ptr<Rhs>(new Rhs("bar")); F(lhs, rhs);

La forma preferida de resolver esto, por supuesto, es usar std::make_shared en std::make_shared lugar.

F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));

Actualización II: Desventaja de std::make_shared

Citando los comentarios de Casey :

Dado que solo hay una asignación, la memoria del titular no se puede desasignar hasta que el bloque de control ya no esté en uso. Un valor weak_ptr puede mantener el bloque de control vivo indefinidamente.

¿Por qué las instancias de weak_ptr s mantienen vivo el bloque de control?

Debe haber una forma para que weak_ptr s determine si el objeto gestionado sigue siendo válido (por ejemplo, para el lock ). Lo hacen comprobando el número de shared_ptr que poseen el objeto gestionado, que se almacena en el bloque de control. El resultado es que los bloques de control están shared_ptr hasta que el recuento de shared_ptr y la cuenta weak_ptr llegan a 0.

Volver a std::make_shared

Como std::make_shared hace una única asignación de std::make_shared tanto para el bloque de control como para el objeto administrado, no hay forma de liberar la memoria para el bloque de control y el objeto administrado de forma independiente. Debemos esperar hasta que podamos liberar tanto el bloque de control como el objeto gestionado, lo que sucede hasta que no hay shared_ptr s ni weak_ptr s con vida.

Supongamos que, en cambio, realizamos dos asignaciones de shared_ptr dinámico para el bloque de control y el objeto gestionado a través de shared_ptr constructor new y shared_ptr . Luego shared_ptr la memoria para el objeto administrado (tal vez antes) cuando no hay vivas shared_ptr , y shared_ptr la memoria para el bloque de control (tal vez más adelante) cuando no hay vivas weak_ptr .


Si necesita una alineación de memoria especial en el objeto controlado por shared_ptr, no puede confiar en make_shared, pero creo que es la única buena razón para no usarlo.


Sobre la eficiencia y el tiempo dedicado a la asignación, hice esta sencilla prueba a continuación, creé muchas instancias a través de estas dos formas (una a la vez):

for (int k = 0 ; k < 30000000; ++k) { // took more time than using new std::shared_ptr<int> foo = std::make_shared<int> (10); // was faster than using make_shared std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10)); }

La cosa es que usar make_shared tomó el doble de tiempo en comparación con el uso de new. Por lo tanto, al usar new hay dos asignaciones de pila en lugar de una que usa make_shared. Tal vez esto sea una prueba estúpida, pero ¿no muestra que usar make_shared lleva más tiempo que usar nuevo? Por supuesto, estoy hablando del tiempo usado solamente.


Veo un problema con std :: make_shared, no soporta constructores privados / protegidos


Shared_ptr : realiza dos asignaciones de pila

  1. Bloque de control (recuento de referencia)
  2. Objeto siendo gestionado

Make_shared : realiza solo una asignación de montón

  1. Control de bloque y datos de objeto.