Diferencia en make_shared y normal shared_ptr en C ++

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

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

¿Alguien puede explicarme paso a paso la secuencia de objetos creados y las operaciones realizadas por ambos para que pueda entender cómo make_sharedes eficiente? He dado un ejemplo arriba como referencia.

Anup Buchke
fuente
44
No es más eficiente. La razón para usarlo es por seguridad de excepción.
Yuushi
Algunos artículos dicen que evita algunos gastos generales de construcción, ¿podría explicar más sobre esto?
Anup Buchke
16
@Yuushi: la seguridad de excepción es una buena razón para usarla, pero también es más eficiente.
Mike Seymour
3
32:15 es donde comienza en el video que he vinculado anteriormente, si eso ayuda.
Chris
44
Ventaja de estilo de código menor: usando make_sharedusted puede escribir auto p1(std::make_shared<A>())y p1 tendrá el tipo correcto.
Ivan Vergiliev

Respuestas:

333

La diferencia es que std::make_sharedrealiza una asignación de montón, mientras que llamar al std::shared_ptrconstructor realiza dos.

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

std::shared_ptr gestiona dos entidades:

  • el bloque de control (almacena metadatos como recuentos de ref, borrador de tipo borrado, etc.)
  • el objeto gestionado

std::make_sharedrealiza 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, new Obj("foo")invoca una asignación de montón para los datos administrados y el std::shared_ptrconstructor 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

NOTA (30/08/2019) : Esto no es un problema desde C ++ 17, debido a los cambios en el orden de evaluación de los argumentos de la función. Específicamente, se requiere que cada argumento de una función se ejecute completamente antes de evaluar otros argumentos.

Dado que el OP parece estar preguntándose sobre el lado de seguridad de excepción de las cosas, he actualizado mi respuesta.

Considera 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 posible orden es:

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

Ahora, supongamos que obtenemos una excepción en el paso 2 (p. Ej., Excepción de memoria insuficiente, el Rhsconstructor arrojó alguna excepción). Luego perdemos la memoria asignada en el paso 1, ya que nada habrá tenido la oportunidad de limpiarla. El núcleo del problema aquí es que el puntero sin formato no se pasó al std::shared_ptrconstructor de inmediato.

Una forma de solucionar esto es hacerlo en líneas separadas para que no pueda ocurrir este orden 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_shareden su 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 :

Como solo hay una asignación, la memoria del usuario no se puede desasignar hasta que el bloque de control ya no esté en uso. A weak_ptrpuede mantener vivo el bloque de control indefinidamente.

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

Debe haber una forma para que weak_ptrs determine si el objeto administrado sigue siendo válido (por ejemplo, for lock). Lo hacen comprobando el número de shared_ptrs que poseen el objeto gestionado, que se almacena en el bloque de control. El resultado es que los bloques de control están vivos hasta que el shared_ptrconteo y el weak_ptrconteo lleguen a 0.

De regreso std::make_shared

Dado que std::make_sharedrealiza una única asignación de almacenamiento dinámico tanto para el bloque de control como para 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 administrado, lo que sucede hasta que no haya shared_ptrs o weak_ptrs vivos.

Supongamos que en vez realizado dos heap-asignaciones para el bloque de control y el objeto gestionado a través de newy shared_ptrconstructor. Luego liberamos la memoria para el objeto administrado (tal vez antes) cuando no hay shared_ptrs vivos, y liberamos la memoria para el bloque de control (tal vez más tarde) cuando no hay weak_ptrs vivos.

mpark
fuente
53
También es una buena idea mencionar la pequeña desventaja del caso de esquina make_shared: dado que solo hay una asignación, la memoria del usuario no se puede desasignar hasta que el bloque de control ya no esté en uso. A weak_ptrpuede mantener vivo el bloque de control indefinidamente.
Casey
14
Otro punto, más estilístico, es: si usa make_sharedy make_uniqueconsistentemente, no tendrá que tener punteros crudos y puede tratar cada ocurrencia newcomo un olor a código.
Philipp
66
Si solo hay uno shared_ptry no weak_ptrs, llamar reset()a la shared_ptrinstancia eliminará el bloque de control. Pero esto es independientemente o si make_sharedse utilizó. El uso make_sharedmarca la diferencia porque podría prolongar la vida útil de la memoria asignada para el objeto administrado . Cuando el shared_ptrrecuento golpea 0, el destructor del objeto gestionado es llamado independientemente de make_shared, pero liberando su memoria sólo puede hacerse si make_sharedse no se utiliza. Espero que esto lo aclare más.
mpark
44
También vale la pena mencionar que make_shared puede aprovechar la optimización "We Know Where You Live" que permite que el bloque de control sea un puntero más pequeño. (Para más detalles, vea la presentación GN2012 de Stephan T. Lavavej en el minuto 12.) make_shared no solo evita una asignación, sino que también asigna menos memoria total.
KnowItAllWannabe
1
@HannaKhalil: ¿Es este quizás el reino de lo que estás buscando ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark
26

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_sharedpuede asignar un solo bloque de memoria para contener 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 make_sharedsignifica que no necesita tratar con newpunteros sin procesar, lo que brinda 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.

Mike Seymour
fuente
2
Entendí tu primer punto correctamente. ¿Puede por favor elaborar o dar algunos enlaces sobre el segundo punto sobre seguridad de excepción?
Anup Buchke
22

Hay 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 poder acceder 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){}
};
Dr_Sam
fuente
Me encontré con este problema exacto y decidí usarlo new, de lo contrario lo habría usado make_shared. Aquí hay una pregunta relacionada al respecto: stackoverflow.com/questions/8147027/… .
jigglypuff
6

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.

Simon Ferquel
fuente
2
Una segunda situación en la que make_shared es inapropiado es cuando desea especificar un borrador personalizado.
KnowItAllWannabe
5

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

Icebeat
fuente
3

Shared_ptr: Realiza dos asignaciones de montón

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

Make_shared: Realiza solo una asignación de almacenamiento dinámico

  1. Bloque de control y datos de objeto.
James
fuente
0

Sobre la eficiencia y el tiempo dedicado a la asignación, realicé esta simple 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 cuestión es que usar make_shared tomó el doble de tiempo en comparación con usar new. Entonces, usando new hay dos asignaciones de montón en lugar de una usando make_shared. Tal vez esta es una prueba estúpida, pero ¿no muestra que usar make_shared lleva más tiempo que usar new? Por supuesto, solo estoy hablando del tiempo utilizado.

orlando
fuente
44
Esa prueba es algo inútil. ¿Se realizó la prueba en la configuración de lanzamiento con optimizaciones? Además, todos sus artículos se liberan inmediatamente, por lo que no es realista.
Phil1970
0

Creo que la parte de seguridad de excepción de la respuesta de mr mpark sigue siendo una preocupación válida. al crear un shared_ptr como este: shared_ptr <T> (nueva T), la nueva T puede tener éxito, mientras que la asignación del bloque de control de shared_ptr puede fallar. en este escenario, la T recién asignada se perderá, ya que shared_ptr no tiene forma de saber que se creó en el lugar y es seguro eliminarla. ¿O me estoy perdiendo algo? No creo que las reglas más estrictas sobre la evaluación de parámetros de función ayuden de ninguna manera aquí ...

Martin Vorbrodt
fuente