¿Debe enable_shared_from_this ser la primera clase base?

8

Mi clase hereda de múltiples bases, una de las cuales es std::enable_shared_from_this. ¿Debe ser la primera base?

Supongamos el siguiente código de ejemplo:

struct A { ~A(); };
struct B { ~B(); };
struct C : A, B, std::enable_shared_from_this<C> {};

std::make_shared<C>(); 

Cuándo ~A()y ~B()ejecutar, ¿puedo estar seguro de que el almacenamiento donde Cvivía todavía está presente?

Filipp
fuente
1
¿Por qué sientes que el orden de destrucción es importante? El destructor de std::enable_shared_from_thisno hace mucho. Su ejemplo se ve bien para mí (suponiendo que no está tratando de hacer algo inteligente en ~Ay ~B, al igual que hacia abajo de fundición a presión thisa C*)
Igor Tandetnik
1
@SM Esto no es un problema de acceso. Sé que enable_shared_from_thisdebe ser una base accesible e inequívoca. En mi ejemplo, lo es. Ces una estructura Hereda públicamente.
Filipp
1
Sí, pero el acceso base está determinado por lo que hereda, no por lo que se hereda. Puedo cambiar mi ejemplo si quieres. El código real en el que se basa utiliza classy public. Elegí structel ejemplo para evitar escribir.
Filipp
44
Así habló The Standard: " [util.smartptr.weak.dest] ~weak_ptr(); Efectos: destruye este weak_ptrobjeto pero no tiene ningún efecto sobre el objeto al que apunta su puntero almacenado". El énfasis es mío.
Igor Tandetnik
1
@Filipp La vida útil del objeto almacenado finaliza cuando el último shared_ptrmuere. Incluso si weak_ptrevita que el bloque de control sea desasignado, no creo que importe.
HolyBlackCat

Respuestas:

1

Cuándo ~A()y ~B()ejecutar, ¿puedo estar seguro de que el almacenamiento donde Cvivía todavía está presente?

¡Por supuesto! Sería difícil usar una clase base que intente liberar su propia memoria (la memoria donde reside). No estoy seguro de que sea formalmente legal.

Las implementaciones no hacen eso: cuando shared_ptr<T>se destruye o restablece a, el recuento de referencia (RC) para la propiedad compartida de Tdisminuye (atómicamente); si alcanzó 0 en la disminución, Tse inicia la destrucción / eliminación de .

Luego, el conteo de propietarios débiles o T existe se disminuye (atómicamente), ya que Tya no existe: necesitamos saber si somos la última entidad interesada en el bloque de control; si la disminución dio un resultado distinto de cero, significa weak_ptrque existen algunos que comparten (podría ser 1 acción o 100%) la propiedad del bloque de control, y ahora son responsables de la desasignación.

De cualquier manera, la disminución atómica terminará en algún momento con un valor cero, para el último copropietario.

Aquí no hay hilos, no no determinismo, y obviamente el último weak_ptr<T>fue destruido durante la destrucción de C. (La suposición no escrita en su pregunta es que no weak_ptr<T>se guardó ninguna otra ).

La destrucción siempre ocurre en ese orden exacto . El bloque de control se usa para la destrucción, ya que no se shared_ptr<T>sabe (en general) qué destructor (potencialmente no virtual) de la clase más derivada (potencialmente diferente) para llamar . (El bloque de control también sabe que no se debe desasignar la memoria en el recuento compartido que llega a cero make_shared).

La única variación práctica entre las implementaciones parece ser sobre los detalles finos de las vallas de memoria y evitar algunas operaciones atómicas en casos comunes.

curioso
fuente
¡Esta es la respuesta que estaba buscando! ¡Gracias! La clave es que el objeto que está vivo en realidad cuenta como un uso débil implícito. Es decir, weak_count1 para un objeto que se make_sharededitó incluso si no hay weak_ptrs. Liberando solo los shared_ptrprimeros decretos use_count. Si se convirtió en 0, el objeto (pero no el bloque de control) se destruye. Luego weak_count se disminuye, y si 0 el bloque de control se destruye + libera. Un objeto que hereda de enable_shared_from_thiscomienza con weak_count= 2. Una solución brillante por parte de los implementadores de STL, como se esperaba.
Filipp
Solo un pequeño detalle pedante: STL es la Biblioteca de plantillas estándar, que a excepción de los artefactos históricos (HP STL o SGI STL) solo se define de manera informal; se trata de tipos que siguen los requisitos de Contenedores, Iteradores y los "algoritmos" que funcionan en ellos. El STL no se limita estrictamente a las plantillas, ya que utiliza algunas clases que no son plantillas (por ejemplo, ej random_access_iterator_tag.). Existe un acuerdo informal para llamar a todo lo relacionado con contenedores como parte del STL. tl; dr: no todas las plantillas en la biblioteca estándar son parte del STL y no todas las plantillas que no están fuera de él.
curioso
5

Cuando se ejecutan ~ A () y ~ B (), ¿puedo estar seguro de que el almacenamiento donde vivía C todavía está presente?

No, y el orden de las clases base es irrelevante. Incluso el uso (o no) de enable_shared_from_this es irrelevante.

Cuando se destruye un objeto C (sin importar lo que ocurra), ~C()se llamará antes que ambos ~A()y ~B(), ya que esa es la forma en que funcionan los destructores de bases. Si intenta "reconstruir" el objeto C en cualquier destructor base y acceder a los campos en él, esos campos ya se habrán destruido, por lo que obtendrá un comportamiento indefinido.

Chris Dodd
fuente
No responde mi pregunta En ninguna parte menciono el intento de "reconstruir" cualquier C. La respuesta debe ser una de " enable_shared_from_thispuede aparecer en cualquier lugar de la lista base, se requieren implementaciones para liberar memoria después de la destrucción de todo el objeto, sin importar cómo se hereda de enable_shared_from_this", o "It debe ser la primera base, heredar en cualquier otro lugar es UB ", o" Este comportamiento no está especificado, o la calidad de la implementación ".
Filipp
@Filipp: La respuesta es una combinación: pueden aparecer en cualquier lugar y, independientemente, una implementación es libre de liberar memoria para parte de un objeto después de la destrucción de esa parte del objeto (y antes de la destrucción de las clases base). Simplemente no existe el requisito de que la memoria solo se pueda liberar después de la destrucción de todo el objeto, independientemente.
Chris Dodd
-1

Si crea un objeto c de tipo C, con bases A, B y un contador de referencia heredando de la base enable_shared_from_this<T>, en primer lugar se asigna memoria para todo el objeto resultante, incluidas las bases en general y la base enable_shared_from_this<T>. El objeto no se destruirá hasta que el último propietario (también conocido como shared_ptr) renuncie a la propiedad. En ese momento ~ enable_shared ..., ~ B y ~ A se ejecutarán después de ~ C. Todavía se garantiza que la memoria asignada completa estará allí hasta que se ejecute el último destructor ~ A. Después de ejecutar ~ A, la memoria completa del objeto se libera de una sola vez. Entonces para responder a su pregunta:

Cuando se ejecutan ~ A () y ~ B (), ¿puedo estar seguro de que el almacenamiento donde vivía C todavía está presente?

Sí, aunque no puede acceder legalmente a él, pero ¿por qué necesitaría saberlo? ¿Qué problema estás tratando de evitar?

Andreas_75
fuente
Lo que escribiste es cierto, pero no responde mi pregunta. Por supuesto, los destructores de clase base se ejecutan después de la clase derivada. Me pregunto si las implementaciones de shared_ptr, weak_ptry enable_shared_from_thisson necesarias para mantener la memoria el tiempo suficiente para que esto sea seguro, incluso cuando enable_shared_from_thisno es la primera base.
Filipp
Ah ok Mirando su pregunta original: no está claro 'esto' [como en "hacer que esto guarde" en su comentario anterior] que está tratando de lograr. Editaré mi respuesta para reflejar la pregunta tal como la entiendo en este momento.
Andreas_75
Haga esto seguro = heredar de enable_shared_from_thisdespués de otra clase base.
Filipp