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:
-
new Lhs("foo"))
-
new Rhs("bar"))
-
std::shared_ptr<Lhs>
-
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
- Bloque de control (recuento de referencia)
- Objeto siendo gestionado
Make_shared
: realiza solo una asignación de montón
- Control de bloque y datos de objeto.