c++ - smart - unique pointer
¿Cómo se implementa std:: tr1:: shared_ptr? (4)
¿Cómo se implementa el conteo de referencias?
Una implementación de puntero inteligente se puede deconstruir, utilizando el diseño de clase basado en políticas 1 , en:
Política de almacenamiento
Política de propiedad
Política de conversión
Política de verificación
Incluido como parámetros de plantilla. Las estrategias de propiedad popular incluyen: copia profunda, conteo de referencias, enlace de referencia y copia destructiva.
El recuento de referencias realiza un seguimiento del número de punteros inteligentes que apuntan a (que poseen 2 ) el mismo objeto. Cuando el número llega a cero, el objeto de puntuación se elimina 3 . El contador real podría ser:
- Compartido entre objetos de puntero inteligente, donde cada puntero inteligente contiene un puntero al contador de referencia:
- Incluido solo en una estructura adicional que agrega un nivel adicional de direccionamiento indirecto al objeto pointee. Aquí, la sobrecarga de espacio de mantener un contador en cada puntero inteligente se intercambia con una velocidad de acceso más lenta:
Contenido dentro del propio objeto Pointee: conteo intrusivo de referencias. La desventaja es que el objeto debe construirse a priori con facilidades para contar:
Finalmente, el método en su pregunta, el recuento de referencias usando listas doblemente enlazadas se llama enlace de referencia y:
... [1] se basa en la observación de que realmente no necesita el recuento real de objetos de puntero inteligente que apuntan a un objeto de puntaje; solo necesitas detectar cuando el conteo baja a cero. Esto lleva a la idea de mantener una "lista de propiedad":
La ventaja de la vinculación de referencia sobre el recuento de referencias es que la primera no utiliza almacén libre adicional, lo que lo hace más confiable: la creación de un puntero inteligente vinculado a referencia no puede fallar. La desventaja es que el enlace de referencia necesita más memoria para su contabilidad (tres punteros frente a un solo puntero más un entero). Además, el recuento de referencias debería ser un poco más rápido: cuando copia punteros inteligentes, solo se necesita una indirección y un incremento. La gestión de la lista es un poco más elaborada. En conclusión, debe usar enlaces de referencia solo cuando la tienda libre es escasa. De lo contrario, prefiera el recuento de referencias.
Respecto a tu segunda pregunta:
¿
std::shared_ptr
(std::shared_ptr
) una lista doblemente enlazada?
Todo lo que pude encontrar en el estándar de C ++ fue:
20.7.2.2.6 creación shared_ptr
...
7. [Nota: estas funciones normalmente asignarán más memoria quesizeof(T)
para permitir estructuras de contabilidad internas como los recuentos de referencia. "Nota final"
Lo que, en mi opinión, excluye las listas con doble enlace, ya que no contienen el recuento real.
Tu tercera pregunta:
¿Hay alguna trampa para usar
std::shared_ptr
?
La gestión de referencias, ya sea contando o enlazando, es una víctima de la fuga de recursos conocida como referencia cíclica . Tengamos un objeto A que contenga un puntero inteligente a un objeto B. Además, el objeto B mantiene un puntero inteligente a A. Estos dos objetos forman una referencia cíclica; aunque ya no uses ninguno de ellos, se usan el uno al otro. La estrategia de gestión de referencias no puede detectar dichas referencias cíclicas, y los dos objetos permanecen asignados para siempre.
Debido a que la implementación de shared_ptr
utiliza el recuento de referencias, las referencias cíclicas son potencialmente un problema. Una cadena de shared_ptr
cíclica se puede romper cambiando el código para que una de las referencias sea un weak_ptr
. Esto se hace asignando valores entre los punteros compartidos y los débiles, pero un puntero débil no afecta el recuento de referencia. Si los únicos punteros que apuntan a un objeto son débiles, el objeto se destruye.
1. Cada característica de diseño con implementaciones múltiples si se formula como política.
2. Los punteros inteligentes son similares a los punteros que apuntan a objetos asignados con new
, no solo apuntan a ese objeto sino que también son responsables de su destrucción y con la liberación de la memoria que ocupa.
3. Sin más problemas, si no se utilizan otros punteros en bruto y / o lo señalan.
[1] Diseño C ++ moderno: Programación genérica y patrones de diseño aplicados. Andrei Alexandrescu, 01 de febrero de 2001
He estado pensando en usar punteros compartidos y sé cómo implementar uno. No quiero hacerlo, así que estoy probando std::tr1::shared_ptr
, y tengo un par de preguntas ...
¿Cómo se implementa el conteo de referencias? ¿Utiliza una lista doblemente enlazada? (Por cierto, ya he buscado en Google, pero no puedo encontrar nada confiable).
¿Hay alguna trampa para usar std::tr1::shared_ptr
?
¿Hay alguna trampa para usar
std::tr1::shared_ptr
?
Sí, si crea ciclos en sus punteros de memoria compartida, entonces la memoria administrada por el puntero inteligente no se reciclará cuando el último puntero salga del alcance porque todavía hay referencias al puntero (es decir, los ciclos causan el recuento de referencias). no bajar a cero).
Por ejemplo:
struct A
{
std::shared_ptr<A> ptr;
};
std::shared_ptr<A> shrd_ptr_1 = std::make_shared(A());
std::shared_ptr<B> shrd_ptr_2 = std::make_shared(A());
shrd_ptr_1->ptr = shrd_ptr_2;
shrd_ptr_2->ptr = shrd_ptr_1;
Ahora, incluso si shrd_ptr_1
y shrd_ptr_2
fuera del alcance, la memoria que están administrando no se ptr
porque el miembro ptr
de cada uno está apuntando entre sí. Si bien este es un ejemplo muy ingenuo de tal ciclo de memoria, puede, si usa este tipo de punteros sin ninguna disciplina, ocurrir de una manera mucho más infame y difícil de rastrear. Por ejemplo, podría ver dónde intentar implementar una lista enlazada circular donde cada puntero next
es un std::shared_ptr
, si no es demasiado cuidadoso, podría ocasionar problemas.
Si desea ver todos los detalles sangrientos, puede echar un vistazo a la implementación boost shared_ptr
:
https://github.com/boostorg/smart_ptr
El recuento de referencias parece implementarse generalmente con un contador y con instrucciones atómicas de incremento / decremento específicas de la plataforma o un bloqueo explícito con un mutex (consulte los atomic_count_*.hpp
en el espacio de nombres detallado ).
shared_ptr
debe gestionar un contador de referencia y el transporte de un functor de eliminación que se deduce por el tipo del objeto dado en la inicialización.
La clase shared_ptr
normalmente aloja dos miembros: una T*
(que es devuelta por operator->
y sin referencia en el operator*
) y una aux*
donde aux
es una clase abstracta interna que contiene:
- un contador (incrementado / decrementado al copiar / asignar / destruir)
- lo que sea necesario para hacer un incremento / decremento atómico (no es necesario si está disponible una plataforma atómica específica INC / DEC)
- una
virtual destroy()=0;
abstractavirtual destroy()=0;
- Un destructor virtual.
Dicha clase aux
(el nombre real depende de la implementación) se deriva de una familia de clases templatizadas (parametrizadas en el tipo dado por el constructor explícito, por ejemplo U
derivado de T
), que agrega:
- un puntero al objeto (igual que
T*
, pero con el tipo real: esto es necesario para administrar adecuadamente todos los casos en queT
es una base para cualquierU
tenga variasT
en la jerarquía de derivación) - una copia del objeto
deletor
dado como política de eliminación al constructor explícito (o eldeletor
predeterminado simplemente haciendo eliminarp
, dondep
es laU*
arriba) - la anulación del método de destrucción, llamando al functor deleter.
Un boceto simplificado puede ser este:
template<class T>
class shared_ptr
{
struct aux
{
unsigned count;
aux() :count(1) {}
virtual void destroy()=0;
virtual ~aux() {} //must be polymorphic
};
template<class U, class Deleter>
struct auximpl: public aux
{
U* p;
Deleter d;
auximpl(U* pu, Deleter x) :p(pu), d(x) {}
virtual void destroy() { d(p); }
};
template<class U>
struct default_deleter
{
void operator()(U* p) const { delete p; }
};
aux* pa;
T* pt;
void inc() { if(pa) interlocked_inc(pa->count); }
void dec()
{
if(pa && !interlocked_dec(pa->count))
{ pa->destroy(); delete pa; }
}
public:
shared_ptr() :pa(), pt() {}
template<class U, class Deleter>
shared_ptr(U* pu, Deleter d) :pa(new auximpl<U,Deleter>(pu,d)), pt(pu) {}
template<class U>
explicit shared_ptr(U* pu) :pa(new auximpl<U,default_deleter<U> >(pu,default_deleter<U>())), pt(pu) {}
shared_ptr(const shared_ptr& s) :pa(s.pa), pt(s.pt) { inc(); }
template<class U>
shared_ptr(const shared_ptr<U>& s) :pa(s.pa), pt(s.pt) { inc(); }
~shared_ptr() { dec(); }
shared_ptr& operator=(const shared_ptr& s)
{
if(this!=&s)
{
dec();
pa = s.pa; pt=s.pt;
inc();
}
return *this;
}
T* operator->() const { return pt; }
T& operator*() const { return *pt; }
};
Donde se weak_ptr
interoperabilidad weak_ptr
se requiere un segundo contador ( weak_count
) en aux
(se incrementará / disminuirá con weak_ptr
), y la delete pa
debe ocurrir solo cuando ambos contadores llegan a cero.