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

shared_ptr - smart pointers c++



¿Por qué son necesarios dos punteros crudos para el objeto gestionado en la implementación std:: shared_ptr? (3)

La razón de esto es que puede tener un shared_ptr que apunta a algo más de lo que posee, y eso es por diseño. Esto se implementa utilizando el constructor enumerado como nr. 8 en cppreference :

template< class Y > shared_ptr( const shared_ptr<Y>& r, T *ptr );

Un shared_ptr creado con este constructor comparte propiedad con r , pero apunta a ptr . Considere esto (código artificial pero ilustrativo):

std::shared_ptr<int> creator() { using Pair = std::pair<int, double>; std::shared_ptr<Pair> p(new Pair(42, 3.14)); std::shared_ptr<int> q(p, &(p->first)); return q; }

Una vez que esta función sale, solo un puntero al subobjeto int del par está disponible para el código del cliente. Pero debido a la propiedad compartida entre q y p , el puntero q "mantiene vivo" todo el objeto Pair .

Una vez que se supone que ocurre la desasignación, el puntero debe pasar todo el objeto Pair al eliminador. Por lo tanto, el puntero al objeto Pair debe estar almacenado en algún lugar junto con el eliminador; en otras palabras, en el bloque de control.

Para un ejemplo menos artificial (probablemente incluso uno más cercano a la motivación original para la característica), considere el caso de señalar a una clase base. Algo como esto:

struct Base1 { // ::: }; struct Base2 { // ::: }; struct Derived : Base1, Base2 { // ::: }; std::shared_ptr<Base2> creator() { std::shared_ptr<Derived> p(new Derived()); std::shared_ptr<Base2> q(p, static_cast<Base2*>(p.get())); return q; }

Por supuesto, la implementación real de std::shared_ptr tiene todas las conversiones implícitas en su lugar para que el baile p and- q en el creator no sea necesario, pero lo he mantenido allí para parecerse al primer ejemplo.

Aquí hay una cita de la sección de nota de implementación de cppreference de std::shared_ptr , que menciona que hay dos punteros diferentes (como se muestra en negrita): el que puede ser devuelto por get() , y el que contiene los datos reales dentro del control bloquear.

En una implementación típica, std::shared_ptr solo tiene dos punteros:

  1. el puntero almacenado (uno devuelto por get() )
  2. un puntero al bloque de control

El bloque de control es un objeto asignado dinámicamente que contiene:

  1. un puntero al objeto administrado o el objeto administrado en sí mismo
  2. el eliminador (tipo borrado)
  3. el asignador (tipo borrado)
  4. el número de shared_ptrs que posee el objeto administrado
  5. la cantidad de weak_ptrs que hacen referencia al objeto gestionado

El puntero sostenido por el shared_ptr directamente es el que devuelve get() , mientras que el puntero u objeto del bloque de control es el que se eliminará cuando el número de propietarios compartidos llegue a cero. Estos indicadores no son necesariamente iguales.

Mi pregunta es, ¿por qué son necesarios dos punteros diferentes (los dos en negrita) para el objeto gestionado (además del puntero al bloque de control)? ¿No es suficiente el que devuelve get() ? ¿Y por qué estos indicadores no son necesariamente iguales?


Una necesidad ineludible de un bloque de control es apoyar punteros débiles. No siempre es posible notificar a todos los indicadores débiles sobre la destrucción de un objeto (de hecho, casi siempre es inviable). En consecuencia, los indicadores débiles necesitan algo a lo que apuntar hasta que todos se hayan ido. Por lo tanto, algún bloque de memoria tiene que andar por ahí. Ese bloque de memoria es el bloque de control. A veces se pueden asignar juntos, pero asignarlos por separado le permite reclamar un objeto potencialmente caro mientras se mantiene cerca del bloque de control económico.

La regla general es que el bloque de control persiste siempre que exista un único puntero compartido o un puntero débil que se refiera a él, mientras que el objeto puede recuperarse en el instante en que no haya punteros compartidos apuntando hacia él.

Esto también permite casos en los que el objeto pasa a propiedad compartida después de su asignación. make_shared puede agrupar estos dos conceptos en un bloque de memoria, pero shared_ptr<T>(new T) primero debe asignar T, y luego descubrir cómo compartirlo después del hecho. Cuando esto no es deseable, boost tiene un concepto relacionado de intrusive_ptr que hace su recuento de referencia directamente dentro del objeto en lugar de hacerlo con un bloque de control (usted tiene que escribir operadores de incremento y decremento para hacer que esto funcione).

He visto implementaciones de punteros compartidos que no tienen un bloque de control. En cambio, los indicadores compartidos desarrollan una lista enlazada entre ellos. Siempre que la lista enlazada contenga 1 o más shared_ptrs, el objeto aún está activo. Sin embargo, este enfoque es más complicado en un escenario de subprocesamiento múltiple porque debe mantener la lista enlazada en lugar de un simple recuento de ref. También es probable que su tiempo de ejecución sea peor en muchos escenarios en los que está asignando y reasignando shared_ptrs repetidamente, porque la lista enlazada es más pesada.

También es posible que una implementación de alto rendimiento agrupe los bloques de control, lo que hace que el costo de su uso sea prácticamente nulo.


Veamos un std::shared_ptr<int> Este es un puntero inteligente contado de referencia a un int* . Ahora el int* no contiene información de recuento de referencia y el objeto shared_ptr sí no puede contener la información de recuento de referencia, ya que puede ser destruido mucho antes de que el recuento de referencia descienda a cero.

Esto significa que debemos tener un objeto intermedio para mantener la información de control que se garantiza que permanecerá persistente hasta que el recuento de referencia se reduzca a cero.

Una vez dicho esto, si crea shared_ptr con make_shared tanto int como el bloque de control se crearán en la memoria contigua, por lo que la desreferenciación será mucho más eficiente.