El artículo analiza las causas y los métodos para evitar un comportamiento indefinido al acceder a un singleton en c ++ moderno. Se proporcionan ejemplos de código de subproceso único. Nada específico del compilador, todo de acuerdo con el estándar.
Introduccion
Para comenzar, le recomiendo que lea otros artículos sobre singleton en Habré:
Tres edades del patrón SingletonSingleton e instancias comunes3 maneras de romper el principio de responsabilidad únicaSingleton: ¿patrón o antipatrón?Usando el patrón singletonY, finalmente, un artículo que tocó el mismo tema, pero se deslizó (aunque solo sea porque no se consideraron las desventajas y limitaciones):
objetos marcados (es decir, objetos
Singleton y vida útil del objetoSiguiente:
- Este no es un artículo sobre las propiedades arquitectónicas de Singleton;
- este no es un artículo "cómo hacer un singleton blanco y esponjoso de un singleton terrible y terrible";
- Esta no es una campaña única;
- no es una cruzada contra singleton;
- Este no es un artículo final feliz.
Este artículo trata sobre un aspecto muy importante, pero aún técnico, del uso de singleton en C ++ moderno. La atención principal en el artículo se presta al momento de la destrucción del singleton, como En la mayoría de las fuentes, el tema de la destrucción está poco divulgado. Por lo general, el énfasis está en el momento en que se creó el singleton, y sobre la destrucción, en el mejor de los casos, dice algo así como "destruido en el orden inverso".
Le pediré que siga el alcance del artículo en los comentarios, especialmente para no organizar el holivar "patrón singleton versus antipatrón singleton".Entonces vamos.
Lo que dice el estándar
Las citas son del borrador final C ++ 14 N3936, como los borradores disponibles de C ++ 17 no están marcados como "final".
Doy la sección más importante en su totalidad. Lugares importantes son destacados por mí.
3.6.3 Terminación [basic.start.term]
1. Los destructores (12.4) para objetos inicializados (es decir, objetos cuya vida útil (3.8) ha comenzado) con una duración de almacenamiento estático se invocan como resultado de regresar de main y como resultado de llamar a std :: exit (18.5). Los destructores para objetos inicializados con una duración de almacenamiento de subprocesos dentro de un subproceso dado se invocan como resultado de regresar de la función inicial de ese subproceso y como resultado de que ese subproceso llame a std :: exit. Las terminaciones de los destructores para todos los objetos inicializados con duración de almacenamiento de subprocesos dentro de ese subproceso se secuencian antes del inicio de los destructores de cualquier objeto con duración de almacenamiento estático. Si la finalización del constructor o la inicialización dinámica de un objeto con duración de almacenamiento de subprocesos se secuencia antes que la de otro, la finalización del destructor del segundo se secuencia antes del inicio del destructor del primero. Si la finalización del constructor o la inicialización dinámica de un objeto con una duración de almacenamiento estático se secuencia antes que la de otro, la finalización del destructor del segundo se secuencia antes del inicio del destructor del primero. [Nota: esta definición permite la destrucción concurrente. –Nota final] Si un objeto se inicializa estáticamente, el objeto se destruye en el mismo orden que si el objeto se inicializara dinámicamente. Para un objeto de tipo matriz o clase, todos los subobjetos de ese objeto se destruyen antes de que se destruya cualquier objeto de alcance de bloque con una duración de almacenamiento estático inicializado durante la construcción de los subobjetos. Si la destrucción de un objeto con una duración de almacenamiento estática o de subprocesos sale a través de una excepción, se llama std :: terminate (15.5.1).
2. Si una función contiene un objeto de alcance de bloque de duración de almacenamiento estático o de subprocesos que se ha destruido y se llama a la función durante la destrucción de un objeto con duración de almacenamiento estático o de subprocesos, el programa tiene un comportamiento indefinido si el flujo de control pasa a través de la definición del objeto de blockcope previamente destruido. Del mismo modo, el comportamiento no está definido si el objeto de alcance de bloque se usa indirectamente (es decir, a través de un puntero) después de su destrucción.
3. Si la finalización de la inicialización de un objeto con una duración de almacenamiento estático se secuencia antes de una llamada a std :: atexit (consulte "cstdlib", 18.5), la llamada a la función pasada a std :: atexit se secuencia antes de la llamada al destructor para el objeto. Si una llamada a std :: atexit se secuencia antes de completar la inicialización de un objeto con duración de almacenamiento estático, la llamada al destructor para el objeto se secuencia antes de que la llamada a la función pase a std :: atexit. Si una llamada a std :: atexit se secuencia antes de otra llamada a std :: atexit, la llamada a la función pasada a la segunda llamada std :: atexit se secuencia antes de que la llamada a la función pase a la primera llamada std :: atexit .
4. Si no se permite el uso de un objeto o función de biblioteca estándar dentro de los controladores de señal (18.10) que no ocurre antes (1.10) de la destrucción de objetos con una duración de almacenamiento estático y la ejecución de funciones registradas std :: atexit (18.5 ), el programa tiene un comportamiento indefinido. [Nota: Si hay un uso de un objeto con una duración de almacenamiento estático que no ocurre antes de la destrucción del objeto, el programa tiene un comportamiento indefinido. Terminar cada subproceso antes de una llamada a std :: exit o la salida de main es suficiente, pero no necesario, para satisfacer estos requisitos. Estos requisitos permiten a los administradores de subprocesos como objetos de duración de almacenamiento estático. —Nota final]
5. Llamar a la función std :: abort () declarada en "cstdlib" finaliza el programa sin ejecutar ningún destructor y sin llamar a las funciones pasadas a std :: atexit () o std :: at_quick_exit ().
Interpretación:
- la destrucción de objetos con duración de almacenamiento de subprocesos se realiza en el orden inverso de su creación;
- estrictamente después de eso, los objetos con una duración de almacenamiento estático se destruyen y se realizan llamadas a las funciones registradas con std :: atexit en el orden inverso de crear dichos objetos y registrar dichas funciones;
- Un intento de acceder a un objeto destruido con una duración de almacenamiento de subprocesos o una duración de almacenamiento estático contiene un comportamiento indefinido. No se proporciona la reinicialización de dichos objetos.
Nota: las variables globales en el estándar se denominan "variables no locales con duración de almacenamiento estático". Como resultado, resulta que todas las variables globales, todos los singletones (estadísticas locales) y todas las llamadas a std :: atexit caen en una sola cola LIFO a medida que se crean / registran.
La información útil para el artículo también está contenida en la sección
3.6.2 Inicialización de variables no locales [basic.start.init] . Traigo solo lo más importante:
La inicialización dinámica de una variable no local con duración de almacenamiento estático está ordenada o desordenada. [...] Las variables con inicialización ordenada definida dentro de una única unidad de traducción se inicializarán en el orden de sus definiciones en la unidad de traducción.
Interpretación (teniendo en cuenta el texto completo de la sección): las variables globales dentro de una unidad de traducción se inicializan en el orden de declaración.
¿Qué habrá en el código?
Todos los ejemplos de código proporcionados en el artículo se publican en el
github .
El código consta de tres capas, como si estuviera escrito por diferentes personas:
- singleton;
- utilidad (clase usando singleton);
- usuario (variables globales y principales).
Singleton y la utilidad son como una biblioteca de terceros, y el usuario es el usuario.
La capa de utilidad está diseñada para aislar la capa de usuario de la capa singleton. En los ejemplos, el usuario tiene la oportunidad de acceder al singleton, pero actuaremos como si fuera imposible.
El usuario primero hace todo bien, y luego con un movimiento de muñeca todo se rompe. Primero intentamos arreglarlo en la capa de utilidad, y si no funciona, entonces en la capa singleton.
En el código, caminaremos constantemente a lo largo del borde, ahora en el lado claro, luego en la oscuridad. Para facilitar el cambio al lado oscuro, se eligió el caso más difícil: acceder a un singleton desde el destructor de la utilidad.
¿Por qué es el caso de llamar desde el destructor el más difícil? Debido a que se puede llamar al destructor de la utilidad en el proceso de minimizar la aplicación, cuando la pregunta "¿se ha destruido el singleton o aún no?" Se vuelve relevante.
El caso es algún tipo de sintético. En la práctica, no se necesitan llamadas a un singleton desde el destructor. Incluso según sea necesario. Por ejemplo, para registrar la destrucción de objetos.
Se utilizan tres clases de singleton:
- SingletonClassic: sin punteros inteligentes. De hecho, no es directamente bastante clásico, pero definitivamente es el más clásico entre los tres considerados;
- SingletonShared - con std :: shared_ptr;
- SingletonWeak - con std :: weak_ptr.
Todos los singletones son plantillas. El parámetro de plantilla se usa para heredar de él. En la mayoría de los ejemplos, están parametrizados por la clase Payload, que proporciona una función pública para agregar datos a std :: set.
El destructor de utilidades en la mayoría de los ejemplos intenta completar cien valores allí. La salida de diagnóstico a la consola también se usa desde el constructor singleton, el destructor singleton y la instancia ().
¿Por qué tan difícil? Para que sea más fácil entender que estamos en el lado oscuro. Apelar al singleton destruido es un comportamiento indefinido, pero no puede manifestarse de ninguna manera externa. Los valores de relleno en el std :: set destruido tampoco garantizan manifestaciones externas, pero no hay una forma más confiable (de hecho, en GCC bajo Linux en ejemplos incorrectos con el singleton clásico, el std :: set destruido se rellena con éxito y en MSVS bajo Windows - se cuelga). Con un comportamiento indefinido, la salida a la consola puede
no ocurrir. Entonces, en los ejemplos correctos, esperamos la ausencia de acceso a la instancia () después del destructor, así como la ausencia de un bloqueo y la ausencia de un bloqueo, y en los incorrectos, ya sea la presencia de tal apelación, o un bloqueo, o un bloqueo, o todo a la vez en cualquier combinación, o lo que sea.
Singleton clásico
Carga útil.h#pragma once #include <set> class Payload { public: Payload() = default; ~Payload() = default; Payload(const Payload &) = delete; Payload(Payload &&) = delete; Payload& operator=(const Payload &) = delete; Payload& operator=(Payload &&) = delete; void add(int value) { m_data.emplace(value); } private: std::set<int> m_data; };
SingletonClassic.h #pragma once #include <iostream> template<typename T> class SingletonClassic : public T { public: ~SingletonClassic() { std::cout << "~SingletonClassic()" << std::endl; } SingletonClassic(const SingletonClassic &) = delete; SingletonClassic(SingletonClassic &&) = delete; SingletonClassic& operator=(const SingletonClassic &) = delete; SingletonClassic& operator=(SingletonClassic &&) = delete; static SingletonClassic& instance() { std::cout << "instance()" << std::endl; static SingletonClassic inst; return inst; } private: SingletonClassic() { std::cout << "SingletonClassic()" << std::endl; } };
SingletonClassic ejemplo 1
Classic_Example1_correct.cpp #include "SingletonClassic.h" #include "Payload.h" #include <memory> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct int main() { return 0; }
Salida de la consolainstancia ()
SingletonClassic ()
instancia ()
~ SingletonClassic ()
La utilidad llama al singleton en el constructor para garantizar que se cree el singleton antes de que se cree la utilidad.
El usuario crea dos std :: unique_ptr: uno vacío, el segundo contiene la utilidad.
El orden de la creación:
- std vacío :: unique_ptr.
- singleton
- utilidad.
Y en consecuencia, el orden de destrucción:
- utilidad;
- singleton
- std vacío :: unique_ptr.
La llamada del destructor de la utilidad al singleton es correcta.
SingletonClassic ejemplo 2
Todo es igual, pero el usuario lo tomó y lo arruinó todo con una sola línea.
Classic_Example2_incorrect.cpp #include "SingletonClassic.h" #include "Payload.h" #include <memory> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is still the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect return 0; }
Salida de la consolainstancia ()
SingletonClassic ()
~ SingletonClassic ()
instancia ()
Se preserva el orden de creación y destrucción. Parece que todo está quieto. Pero no Al invocar emptyUnique.swap (utilityUnique), el usuario cometió un comportamiento indefinido.
¿Por qué el usuario hizo cosas tan estúpidas? Porque no sabe nada sobre la estructura interna de la biblioteca, que le proporcionó un singleton y utilidad.
¿Y si conoces la estructura interna de la biblioteca? ... de todos modos, en código real es muy fácil involucrarse. Y tienes que salir por dolorosa discusión, porque entender qué sucedió exactamente no será fácil.
¿Por qué no requiere que la biblioteca se use correctamente? Bueno, hay todo tipo de muelles para escribir, ejemplos ... ¿Y por qué no hacer una biblioteca que no sea tan fácil de estropear?
SingletonClassic ejemplo 3
En el curso de la preparación del artículo durante varios días, creí que era imposible eliminar el comportamiento indefinido del ejemplo anterior en la capa de utilidad, y la solución solo estaba disponible en la capa singleton. Pero con el tiempo, sin embargo, surgió una solución.
Antes de abrir los spoilers con el código y la explicación, sugiero al lector que intente encontrar una salida a la situación por su cuenta (¡solo en la capa de utilidad!). No excluyo que haya mejores soluciones.
Classic_Example3_correct.cpp #include "SingletonClassic.h" #include "Payload.h" #include <memory> #include <iostream> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { thread_local auto flag_strong = std::make_shared<char>(0); m_flag_weak = flag_strong; SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { if ( !m_flag_weak.expired() ) { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } } private: std::weak_ptr<char> m_flag_weak; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); { // To demonstrate normal processing before application ends auto utility = ClassicSingleThreadedUtility(); } // Guaranteed destruction order is still the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect ... // ... but utility uses a variable with thread storage duration to detect thread termination. return 0; }
Salida de la consolainstancia ()
SingletonClassic ()
instancia ()
instancia ()
~ SingletonClassic ()
ExplicaciónEl problema solo ocurre al minimizar la aplicación. El comportamiento indefinido puede eliminarse enseñando a la utilidad a reconocer cuándo se minimiza la aplicación. Para hacer esto, utilizamos una variable flag_strong del tipo std :: shared_ptr, que tiene un calificador de duración de almacenamiento de subprocesos (ver extractos del estándar en el artículo anterior): esto es como una estática, pero solo se destruye cuando el subproceso actual termina antes de que se destruya cualquiera de las estadísticas , incluso antes de la destrucción Singleton La variable flag_strong es una para todo el flujo, y cada instancia de la utilidad almacena su copia débil.
En un sentido estricto, la solución se puede llamar un truco, porque es indirecto y no obvio. Además, advierte demasiado pronto y, a veces (en una aplicación multiproceso) generalmente advierte falso. Pero en un sentido amplio, esto no es un truco, sino una solución completamente definida por las propiedades estándar, tanto desventajas como ventajas.
Singletonshared
Pasemos a un singleton modificado basado en std :: shared_ptr.
SingletonShared.h #pragma once #include <memory> #include <iostream> template<typename T> class SingletonShared : public T { public: ~SingletonShared() { std::cout << "~SingletonShared()" << std::endl; } SingletonShared(const SingletonShared &) = delete; SingletonShared(SingletonShared &&) = delete; SingletonShared& operator=(const SingletonShared &) = delete; SingletonShared& operator=(SingletonShared &&) = delete; static std::shared_ptr<SingletonShared> instance() { std::cout << "instance()" << std::endl; // "new" and no std::make_shared because of private c-tor static auto inst = std::shared_ptr<SingletonShared>(new SingletonShared); return inst; } private: SingletonShared() { std::cout << "SingletonShared()" << std::endl; } };
Ay-ah-ah, el nuevo operador no debe usarse en código moderno, ¡en cambio se necesita std :: make_shared! Y esto es evitado por el constructor privado del singleton.
Ja! ¡Yo también tengo un problema! Declare std :: make_shared un singleton freind! ... y obtenga una variación del antipatrón PublicMorozov: usando el mismo std :: make_shared, será posible crear instancias adicionales del singleton que no son provistas por la arquitectura.
SingletonShared Ejemplos 1 y 2
Corresponde totalmente a los ejemplos No. 1 y 2 para la versión clásica. Se hicieron cambios significativos solo en la capa singleton, la utilidad esencialmente permaneció igual. Al igual que en los ejemplos con el singleton clásico, el ejemplo 1 es correcto y el ejemplo 2 muestra un comportamiento indefinido.
Shared_Example1_correct.cpp #include "SingletonShared.h" #include <Payload.h> #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonShared<Payload>::instance(); } ~SharedSingleThreadedUtility() { if ( auto instance = SingletonShared<Payload>::instance() ) for ( int i = 0; i < 100; ++i ) instance->add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct int main() { return 0; }
Salida de la consolainstancia ()
SingletonShared ()
instancia ()
~ SingletonShared ()
Shared_Example2_incorrect.cpp #include "SingletonShared.h" #include "Payload.h" #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonShared<Payload>::instance(); } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( auto instance = SingletonShared::instance() ) // for ( int i = 0; i < 100; ++i ) // instance->add(i); // ... so this code will demonstrate UB in colour auto instance = SingletonShared<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance->add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect return 0; }
Salida de la consolainstancia ()
SingletonShared ()
~ SingletonShared ()
instancia ()
SingletonShared ejemplo 3
Y ahora intentaremos solucionar este problema mejor que en el ejemplo número 3 de los clásicos.
La solución es obvia: solo necesita extender la vida útil del singleton almacenando una copia de std :: shared_ptr devuelta por el singleton en la utilidad. Y esta solución, completa con SingletonShared, ha sido ampliamente replicada en código abierto.
Shared_Example3_correct.cpp #include "SingletonShared.h" #include "Payload.h" #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<Payload>::instance()) { } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( m_singleton ) // for ( int i = 0; i < 100; ++i ) // m_singleton->add(i); // ... so this code will allow to demonstrate UB in colour for ( int i = 0; i < 100; ++i ) m_singleton->add(i); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<Payload>> m_singleton; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); int main() { // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct ... // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect... // ... but utility have made a copy of shared_ptr when it was available, // so it's correct again. return 0; }
Salida de la consolainstancia ()
SingletonShared ()
~ SingletonShared ()
Y ahora, atención, la pregunta es:
¿realmente querías extender la vida de un singleton?¿O deseaba deshacerse del comportamiento indefinido y elegir la extensión de la vida como una forma de permanecer en la superficie?
La incorrección teórica en forma de sustitución de objetivos por medio conduce al riesgo de punto muerto (o referencia cíclica: llámelo como quiera).
Sí nuuuuuu, así es como tienes que esforzarte tanto? Tendrás que pensar en tanto tiempo, ¡y ciertamente no lo harás por accidente!CallbackPayload.h #pragma once #include <functional> class CallbackPayload { public: CallbackPayload() = default; ~CallbackPayload() = default; CallbackPayload(const CallbackPayload &) = delete; CallbackPayload(CallbackPayload &&) = delete; CallbackPayload& operator=(const CallbackPayload &) = delete; CallbackPayload& operator=(CallbackPayload &&) = delete; void setCallback(std::function<void()> &&fn) { m_callbackFn = std::move(fn); } private: std::function<void()> m_callbackFn; };
SomethingWithVeryImportantDestructor.h #pragma once #include <iostream> class SomethingWithVeryImportantDestructor { public: SomethingWithVeryImportantDestructor() { std::cout << "SomethingWithVeryImportantDestructor()" << std::endl; } ~SomethingWithVeryImportantDestructor() { std::cout << "~SomethingWithVeryImportantDestructor()" << std::endl; } SomethingWithVeryImportantDestructor(const SomethingWithVeryImportantDestructor &) = delete; SomethingWithVeryImportantDestructor(SomethingWithVeryImportantDestructor &&) = delete; SomethingWithVeryImportantDestructor& operator=(const SomethingWithVeryImportantDestructor &) = delete; SomethingWithVeryImportantDestructor& operator=(SomethingWithVeryImportantDestructor &&) = delete; };
Shared_Example4_incorrect.cpp #include "SingletonShared.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility()
Salida de la consolainstancia ()
SingletonShared ()
SharedSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()
Se creó un singleton.
Se ha creado una utilidad.
Se creó algo S-Very-Important-Destructor (agregué esto para intimidar, porque en Internet hay publicaciones como "bueno, el destructor de singleton no se llamará, así que qué pasa, debe existir todo el tiempo programas ").
¡Pero no se llamó a ningún destructor para ninguno de estos objetos!
¿Por qué? Debido a la sustitución de goles por medios.
Singletonweak
SingletonWeak.h #pragma once #include <memory> #include <iostream> template<typename T> class SingletonWeak : public T { public: ~SingletonWeak() { std::cout << "~SingletonWeak()" << std::endl; } SingletonWeak(const SingletonWeak &) = delete; SingletonWeak(SingletonWeak &&) = delete; SingletonWeak& operator=(const SingletonWeak &) = delete; SingletonWeak& operator=(SingletonWeak &&) = delete; static std::weak_ptr<SingletonWeak> instance() { std::cout << "instance()" << std::endl; // "new" and no std::make_shared because of private c-tor static auto inst = std::shared_ptr<SingletonWeak>(new SingletonWeak); return inst; } private: SingletonWeak() { std::cout << "SingletonWeak()" << std::endl; } };
Tal modificación del singleton en fuentes abiertas, si se da, ciertamente no es frecuente. Encontré algunas variantes extrañas al revés con un std :: weak_ptr, que parece ser usado, que, al parecer, ofrece a la utilidad nada más que prolongar la vida de un singleton:
La opción que propongo, cuando se aplica correctamente en capas singleton y de utilidad:
- protege contra acciones en la capa de usuario descrita en los ejemplos anteriores, incluida la prevención de puntos muertos;
- determina el momento de plegado de la aplicación con mayor precisión que la aplicación thread_local en Classic_Example3_correct, es decir le permite acercarse al borde;
- No sufro el problema teórico de sustituir objetivos con medios (no sé si puede surgir algo tangible que no sea un punto muerto a partir de este problema teórico).
Sin embargo, hay un inconveniente: extender la vida útil de un singleton
aún puede permitir que
se acerque
aún más al borde.
SingletonWeak ejemplo 1
Similar a Shared_Example3_correct.cpp.
Weak_Example1_correct.cpp #include "SingletonWeak.h" #include "Payload.h" #include <memory> class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<Payload>::instance()) { } ~WeakSingleThreadedUtility() { // Sometimes this check may result as "false" even in case of incorrect usage, // and there's no way to guarantee a demonstration of undefined behaviour in colour if ( auto strong = m_weak.lock() ) for ( int i = 0; i < 100; ++i ) strong->add(i); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<Payload>> m_weak; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of WeakSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<WeakSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<WeakSingleThreadedUtility>(); int main() { // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct ... // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect... // ... but utility have made a weak copy of shared_ptr when it was available, // so it's correct again. return 0; }
Salida de la consolainstancia ()
SingletonWeak ()
~ SingletonWeak ()
¿Por qué necesitamos SingletonWeak, porque nadie molesta a la utilidad para usar SingletonShared como SingletonWeak? Sí, nadie se molesta. E incluso nadie molesta la utilidad de usar SingletonWeak como SingletonShared. Pero usarlos para los fines previstos es un poco más fácil que usarlos para otros fines.
SingletonWeak ejemplo 2
Similar a Shared_Example4_incorrect, pero solo no se produce un punto muerto en este caso.
Weak_Example2_correct.cpp #include "SingletonWeak.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility()
Salida de la consolainstancia ()
SingletonWeak ()
WeakSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()
~ SingletonWeak ()
~ SomethingWithVeryImportantDestructor ()
~ WeakSingleThreadedUtility ()
En lugar de una conclusión
¿Y qué, tal modificación de un singleton eliminará el comportamiento indefinido? Prometí que no habría un final feliz. Los siguientes ejemplos muestran que las acciones de sabotaje habilidosas en la capa de usuario pueden destruir incluso la biblioteca pensada correcta con un singleton (pero debemos admitir que
esto difícilmente se puede hacer por accidente).
Shared_Example5_incorrect.cpp #include "SingletonShared.h" #include "Payload.h" #include <memory> #include <cstdlib> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<Payload>::instance()) { } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( m_singleton ) // for ( int i = 0; i < 100; ++i ) // m_singleton->add(i); // ... so this code will allow to demonstrate UB in colour for ( int i = 0; i < 100; ++i ) m_singleton->add(i); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<Payload>> m_singleton; }; void cracker() { SharedSingleThreadedUtility(); } // 1. Register cracker() using std::atexit // 2. Create singleton // 3. Create utility auto reg = [](){ std::atexit(&cracker); return 0; }(); auto utility = SharedSingleThreadedUtility(); // This guarantee destruction in order: // - utility; // - singleton. // This order is correct. // Additionally, there's a copy of shared_ptr in the class instance... // ... but there was std::atexit registered before singleton, // so cracker() will be invoked after destruction of utility and singleton. // There's second try to create a singleton - and it's incorrect. int main() { return 0; }
Salida de la consolainstancia ()
SingletonShared ()
~ SingletonShared ()
instancia ()
Weak_Example3_incorrect.cpp #include "SingletonWeak.h" #include "Payload.h" #include <memory> #include <cstdlib> class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<Payload>::instance()) { } ~WeakSingleThreadedUtility() { // Sometimes this check may result as "false" even in case of incorrect usage, // and there's no way to guarantee a demonstration of undefined behaviour in colour if ( auto strong = m_weak.lock() ) for ( int i = 0; i < 100; ++i ) strong->add(i); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<Payload>> m_weak; }; void cracker() { WeakSingleThreadedUtility(); } // 1. Register cracker() using std::atexit // 2. Create singleton // 3. Create utility auto reg = [](){ std::atexit(&cracker); return 0; }(); auto utility = WeakSingleThreadedUtility(); // This guarantee destruction in order: // - utility; // - singleton. // This order is correct. // Additionally, there's a copy of shared_ptr in the class instance... // ... but there was std::atexit registered before singleton, // so cracker() will be invoked after destruction of utility and singleton. // There's second try to create a singleton - and it's incorrect. int main() { return 0; }
Salida de la consolainstancia ()
SingletonWeak ()
~ SingletonWeak ()
instancia ()