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

shared_ptr - smart pointers c++



¿Por qué el tamaño de make_shared dos punteros? (4)

Como se ilustra en el código here , el tamaño del objeto devuelto desde make_shared es de dos punteros.

Sin embargo, ¿por qué no make_shared funciona como el siguiente (suponiendo que T es el tipo al que estamos haciendo un puntero compartido)?

El resultado de make_shared es un puntero de tamaño, que apunta a la memoria asignada de tamaño sizeof(int) + sizeof(T) , donde int es un recuento de referencia, y esto se incrementa y disminuye en la construcción / destrucción de los punteros.

unique_ptr s solo tiene el tamaño de un puntero, así que no estoy seguro de por qué el puntero compartido necesita dos. Por lo que puedo decir, todo lo que necesita es un recuento de referencia, que con make_shared , se puede colocar con el objeto en sí.

Además, ¿hay alguna implementación que se implemente de la manera que sugiero (sin tener que meterme con intrusive_ptr s para objetos particulares)? Si no, ¿cuál es la razón por la que se evita la implementación que sugiero?


El recuento de referencia no se puede almacenar en shared_ptr . shared_ptr s tiene que compartir el recuento de referencia entre las distintas instancias, por lo tanto, shared_ptr debe tener un puntero al recuento de referencia. Además, shared_ptr (el resultado de make_shared ) no tiene que almacenar el recuento de referencia en la misma asignación en la que se asignó el objeto.

El punto de make_shared es evitar la asignación de dos bloques de memoria para shared_ptr s. Normalmente, si solo hace shared_ptr<T>(new T()) , tiene que asignar memoria para el recuento de referencia además de la T asignada. make_shared pone todo esto en un bloque de asignación, usando la ubicación nueva y la eliminación para crear la T Así que solo obtienes una asignación de memoria y una eliminación.

Pero shared_ptr aún debe tener la posibilidad de almacenar el recuento de referencia en un bloque de memoria diferente, ya que no es necesario usar make_shared . Por lo tanto necesita dos punteros.

Aunque realmente, esto no debería molestarte. Dos punteros no son mucho espacio, incluso en tierra de 64 bits. Aún está recibiendo la parte importante de la funcionalidad de intrusive_ptr (a saber, no asignar memoria dos veces).

Su pregunta parece ser "¿por qué debería make_shared devolver un shared_ptr lugar de algún otro tipo?" Hay muchas razones.

shared_ptr está pensado para ser un tipo de puntero inteligente predeterminado que se captura a todos. Puede usar un unique_ptr o scoped_ptr para los casos en los que está haciendo algo especial. O simplemente para asignaciones de memoria temporal en el alcance de la función. Pero shared_ptr está destinado a ser el tipo de cosa que se usa para cualquier trabajo de referencia serio contado.

Por eso, shared_ptr sería parte de una interfaz. Tendrías funciones que toman shared_ptr . Tendrías funciones que devuelvan shared_ptr . Y así.

Ingrese make_shared . Bajo su idea, esta función devolvería algún tipo de objeto nuevo, make_shared_ptr o lo que sea. Tendría su propio equivalente a weak_ptr , a make_weak_ptr . Pero a pesar del hecho de que estos dos conjuntos de tipos compartirían exactamente la misma interfaz , no se podrían usar juntos.

Las funciones que toman make_shared_ptr no pueden tomar shared_ptr . Podría hacer que make_shared_ptr convierta en shared_ptr , pero no podría ir al revés. No shared_ptr tomar un shared_ptr y convertirlo en make_shared_ptr , porque shared_ptr necesita tener dos punteros. No puede hacer su trabajo sin dos punteros.

Así que ahora tienes dos conjuntos de punteros que son medio incompatibles. Tienes conversiones unidireccionales; Si tiene una función que devuelve un shared_ptr , es mejor que el usuario esté utilizando shared_ptr lugar de make_shared_ptr .

Hacer esto por el valor del espacio de un puntero simplemente no vale la pena. ¿Creando esta incompatibilidad, creando dos conjuntos de punteros solo para 4 bytes? Eso simplemente no vale la pena el problema que se causa.

Ahora, tal vez le preguntes: "si tiene make_shared_ptr ¿por qué alguna vez necesitaría shared_ptr ?"

Porque make_shared_ptr es insuficiente. make_shared no es la única forma de crear un shared_ptr . Tal vez estoy trabajando con un código C Tal vez estoy usando SQLite3. sqlite3_open devuelve un sqlite3* , que es una conexión de base de datos.

En este momento, utilizando el functor destructor correcto, puedo almacenar ese sqlite3* en un shared_ptr . Ese objeto será referencia contada. Puedo usar weak_ptr donde sea necesario. Puedo jugar todos los trucos que normalmente haría con un shared_ptr C ++ shared_ptr que obtengo de make_shared o de cualquier otra interfaz. Y funcionaría perfectamente.

Pero si make_shared_ptr existe, entonces eso no funciona. Porque no puedo crear uno de ellos a partir de eso. El sqlite3* ya ha sido asignado; No puedo hacerlo mediante make_shared , porque make_shared construye un objeto. No funciona con los ya existentes.

Oh, claro, podría hacer un poco de pirateo, donde sqlite3* el sqlite3* en un tipo C ++ cuyo destructor lo destruirá, luego usar make_shared para crear ese tipo. Pero luego su uso se vuelve mucho más complicado: tienes que pasar por otro nivel de direccionamiento indirecto. Y tienes que pasar por la molestia de hacer un tipo y así sucesivamente; El método destructor de arriba al menos puede usar una función lambda simple.

La proliferación de tipos de punteros inteligentes es algo que se debe evitar. Necesitas uno inmóvil, uno movible y uno copiable compartido. Y una más para romper las referencias circulares de esta última. Si empiezas a tener varios tipos de ese tipo, entonces tienes necesidades muy especiales o estás haciendo algo mal.


En todas las implementaciones que conozco, shared_ptr almacena el puntero propio y el recuento de referencia en el mismo bloque de memoria . Esto es contrario a lo que dicen otras respuestas. Además, una copia del puntero se almacenará en el objeto shared_ptr . N1431 describe el diseño de memoria típico.

Es cierto que se puede construir un puntero contado de referencia con tamaño de solo un puntero. Pero std::shared_ptr contiene características que exigen absolutamente un tamaño de dos punteros. Una de esas características es este constructor:

template<class Y> shared_ptr(const shared_ptr<Y>& r, T *p) noexcept; Effects: Constructs a shared_ptr instance that stores p and shares ownership with r. Postconditions: get() == p && use_count() == r.use_count()

Un puntero en shared_ptr al bloque de control propiedad de r . Este bloque de control contendrá el puntero propio , que no tiene que ser p , y normalmente no es p . El otro puntero en shared_ptr , el que devolvió get() , será p .

Esto se conoce como soporte de aliasing y se introdujo en N2351 . Puede observar que shared_ptr tenía un shared_ptr dos punteros antes de la introducción de esta función. Antes de la introducción de esta característica, uno podría haber implementado shared_ptr con un shared_ptr de un puntero, pero nadie lo hizo porque no era práctico. Después de N2351 , se hizo imposible.

Una de las razones por las que no era práctico antes de N2351 se debía al apoyo para:

shared_ptr<B> p(new A);

Aquí, p.get() devuelve una B* , y generalmente ha olvidado todo sobre el tipo A El único requisito es que A* sea ​​convertible a B* . B puede derivar de A usando herencia múltiple. Y esto implica que el valor del puntero en sí puede cambiar al convertir de A a B y viceversa. En este ejemplo, shared_ptr<B> necesita recordar dos cosas:

  1. Cómo devolver una B* cuando se llama a get() .
  2. Cómo eliminar un A* cuando sea el momento de hacerlo.

Una muy buena técnica de implementación para lograr esto es almacenar el B* en el objeto shared_ptr , y el A* dentro del bloque de control con el recuento de referencia.


Otros ya han dicho que shared_ptr necesita dos punteros porque tiene que apuntar al bloque de memoria de conteo de referencia y al Bloque de memoria de tipos señalados.

Supongo que lo que estás preguntando es esto:

Cuando se usa make_shared ambos bloques de memoria se fusionan en uno, y como los tamaños y la alineación de los bloques se conocen y se fijan en el momento de la compilación, un puntero podría calcularse a partir del otro (porque tienen un desplazamiento fijo). Entonces, ¿por qué el estándar o boost no crea un segundo tipo como small_shared_ptr que solo contiene un puntero? ¿Es eso correcto?

Bueno, la respuesta es que si lo piensas bien, rápidamente se convierte en una gran molestia por muy poco beneficio. ¿Cómo hacer compatibles los punteros? Una dirección, es decir, asignar un small_shared_ptr a shared_ptr sería fácil, al revés extremadamente difícil. Incluso si resuelve este problema de manera eficiente, la pequeña eficiencia que obtiene probablemente se perderá con las conversiones de ida y vuelta que inevitablemente se acumularán en cualquier programa serio. Y el tipo de puntero adicional también hace que el código que lo usa sea más difícil de entender.


Tengo una implementación honey::shared_ptr que se optimiza automáticamente a un tamaño de 1 puntero cuando es intrusivo. Es conceptualmente simple: los tipos que heredan de SharedObj tienen un bloque de control incorporado, por lo que en ese caso shared_ptr<DerivedSharedObj> es intrusivo y puede optimizarse. Unifica boost::intrusive_ptr con punteros no intrusivos como std::shared_ptr y std::weak_ptr .

Esta optimización solo es posible porque no soy compatible con el alias (vea la respuesta de Howard). El resultado de make_shared puede tener un tamaño de puntero si se sabe que T es intrusivo en tiempo de compilación. Pero, ¿y si se sabe que T es intrusivo en tiempo de compilación? En este caso, no es práctico tener un tamaño de puntero, ya que shared_ptr debe comportarse genéricamente para admitir los bloques de control asignados al lado y por separado de sus objetos. Con solo 1 puntero, el comportamiento genérico sería apuntar hacia el bloque de control, por lo que para llegar a T* primero tendría que desreferir el bloque de control, lo que es poco práctico.