Diferencia en make_shared y normal shared_ptr en C ++

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

Muchas publicaciones de google y stackoverflow están ahí, pero no puedo entender por qué make_shared es más eficiente que usar directamente shared_ptr .

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

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

¿Dónde ocurren las asignaciones de montón?

std::shared_ptr administra dos entidades:

  • el bloque de control (almacena metadatos tales como recuentos de ref, eliminador borrado de tipo, etc.)
  • el objeto que se está gestionando

std::make_shared realiza una única contabilidad de asignación de montón 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 montón para los datos administrados y el constructor std::shared_ptr realiza otro para el bloque de control.

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

Actualización I: Excepción-Seguridad

Como el OP parece estar preguntándose sobre el lado de la excepción de seguridad, he actualizado mi respuesta.

Considera este ejemplo,

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

Como C ++ permite un orden arbitrario de evaluación de subexpresiones, un posible ordenamiento es:

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

Ahora, supongamos que obtenemos una excepción lanzada en el paso 2 (p. Ej., Excepción de Rhs de memoria, el constructor de Rhs arrojó 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 sin std::shared_ptr no pasó al constructor std::shared_ptr inmediatamente.

Una forma de arreglar esto es hacerlos en líneas separadas para que este orden arbitario no pueda ocurrir.

 auto lhs = std::shared_ptr(new Lhs("foo")); auto rhs = std::shared_ptr(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("foo"), std::make_shared("bar")); 

Actualización II: desventaja de std::make_shared

Citando los comentarios de Casey :

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

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

Debe haber una manera para weak_ptr s para determinar si el objeto administrado sigue siendo válido (por ejemplo, para el lock ). Lo hacen al verificar el número de shared_ptr s que poseen el objeto administrado, 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 el conteo de weak_ptr lleguen a cero.

Volver a std::make_shared

Como std::make_shared realiza una sola asignación de montón para el bloque de control y el objeto gestionado, no hay forma de liberar la memoria para el bloque de control y el objeto gestionado de forma independiente. Debemos esperar hasta que podamos liberar tanto el bloque de control como el objeto gestionado, que sucede hasta que no haya shared_ptr s o weak_ptr s alive.

Supongamos que en su lugar realizamos dos asignaciones de montón 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 (quizás antes) cuando no hay shared_ptr vivos, y shared_ptr la memoria para el bloque de control (tal vez más adelante) cuando no hay weak_ptr vivos.

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

Además de esta eficacia, el uso de make_shared significa que no necesita tratar con punteros new y sin procesar, ofreciendo 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. .

Hay otro caso donde las dos posibilidades son diferentes, además de las ya mencionadas: si necesita llamar a un constructor no público (protegido o privado), make_shared podría no ser capaz de acceder a él, mientras que la variante con el nuevo funciona bien .

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

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.

Shared_ptr : realiza dos asignaciones de montón

  1. Bloque de control (recuento de referencia)
  2. Objeto que se maneja

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

  1. Controle el bloque y los datos del objeto.

Acerca de la eficiencia y el tiempo dedicado a la asignación, realicé 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 foo = std::make_shared (10); // was faster than using make_shared std::shared_ptr foo2 = std::shared_ptr(new int(10)); } 

El caso es que usar make_shared tomó el doble de tiempo comparado con el uso de new. Entonces, usando new hay dos asignaciones de montón en lugar de una utilizando make_shared. Tal vez esta es una prueba estúpida, pero ¿no muestra que usar make_shared toma más tiempo que usar new? Por supuesto, estoy hablando de tiempo usado solo.

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