C++ Nifty Counter idiom; ¿por qué?
c++11 singleton (4)
Hace poco me encontré con el Nifty Counter Idiom . Entiendo que esto se usa para implementar globales en la biblioteca estándar como cout, cerr, etc. Dado que los expertos lo han elegido, asumo que es una técnica muy fuerte.
Estoy tratando de entender cuál es la ventaja de usar algo más como un Meyer Singleton.
Por ejemplo, uno podría tener, en un archivo de encabezado:
inline Stream& getStream() { static Stream s; return s; }
static Stream& stream = getStream();
La ventaja es que no tiene que preocuparse por el recuento de referencias, la ubicación nueva o tener dos clases, es decir, el código es mucho más simple. Ya que no se hace de esta manera, estoy seguro de que hay una razón:
- ¿No se garantiza que esto tenga un solo objeto global en bibliotecas compartidas y estáticas? Parece que la ODR debería garantizar que solo puede haber una variable estática.
- ¿Hay algún tipo de costo de rendimiento? Parece que tanto en mi código como en el Nifty Counter, estás siguiendo una referencia para llegar al objeto.
- ¿Hay algunas situaciones donde el recuento de referencias es realmente útil? Parece que todavía llevará a que el objeto se construya si se incluye el encabezado y se destruye al final del programa, como Meyer Singleton.
- ¿La respuesta consiste en abrir algo manualmente? No tengo mucha experiencia con eso.
Edit: Se me pidió que escribiera el siguiente código mientras leía la respuesta de Yakk, lo agrego a la pregunta original como una demostración rápida. Es un ejemplo muy mínimo que muestra cómo el uso de Meyer Singleton + una referencia global lleva a la inicialización antes de main: http://coliru.stacked-crooked.com/a/a7f0c8f33ba42b7f .
Con la solución que tiene aquí, la variable de stream
global se asigna en algún momento durante la inicialización estática, pero no se especifica cuándo. Por lo tanto, es posible que no funcione el uso de stream
de otras unidades de compilación durante la inicialización estática. Nifty counter es una forma de garantizar que un global (por ejemplo, std :: cout) se puede utilizar incluso durante la inicialización estática.
#include <iostream>
struct use_std_out_in_ctor
{
use_std_out_in_ctor()
{
// std::cout guaranteed to be initialized even if this
// ctor runs during static initialization
std::cout << "Hello world" << std::endl;
}
};
use_std_out_in_ctor global; // causes ctor to run during static initialization
int main()
{
std::cout << "Did it print Hello world?" << std::endl;
}
La referencia estática local / Meyer singleton + static global (su solución) es casi equivalente al contador ingenioso.
Las diferencias son las siguientes:
No se requiere ningún archivo .cpp en su solución.
Técnicamente, el
static Steam&
existe en cada unidad de compilación; el objeto al que se hace referencia no lo hace. Como no hay forma de detectar esto en la versión actual de C ++, en as-if esto desaparece. Pero algunas implementaciones podrían realmente crear esa referencia en lugar de ignorarla.Alguien podría llamar a
getStream()
antes de lagetStream()
static Stream&
está creando; esto causaría dificultades en el orden de destrucción (con la secuencia que se destruye más tarde de lo esperado). Esto puede evitarse haciendo eso contra las reglas.El estándar tiene el mandato de hacer que la creación de la
static Stream
local en el subprocesogetStream
eninline
getStream
segura. Detectar que esto no va a suceder es un desafío para el compilador, por lo que puede haber una sobrecarga redundante de seguridad de subprocesos en su solución. El contador ingenioso no admite explícitamente la seguridad de subprocesos; esto se considera seguro ya que se ejecuta en el momento de la inicialización estática, antes de que se esperen subprocesos.La llamada a
getStream()
debe ocurrir en todas y cada una de las unidades de compilación. Solo si se comprueba que no puede hacer nada, puede optimizarse, lo que es difícil. El ingenioso contador tiene un costo similar, pero la operación puede o no ser más simple de optimizar o en costo de tiempo de ejecución. (Para determinar esto, será necesario inspeccionar el resultado de ensamblaje resultante en una variedad de compiladores)"estática mágica" (estáticas locales sin condiciones de raza) donde se introdujo en C ++ 11. Podría haber otros problemas antes de C ++ 11 magic statics con su código; El único en el que puedo pensar es en alguien que llama a
getStream()
directamente en otro hilo durante la inicialización estática, que (como se mencionó anteriormente) debería prohibirse en general.Fuera del ámbito del estándar, su versión creará de forma automática y mágica un nuevo singleton en cada fragmento de código enlazado dinámicamente (DLL, .so, etc.). El ingenioso contador solo creará el singleton en el archivo cpp. Esto puede dar al escritor de la biblioteca un control más estricto sobre la generación accidental de nuevos singletons; pueden pegarlo en la biblioteca dinámica, en lugar de generar duplicados.
Evitar tener más de un singleton a veces es importante.
Resumiendo las respuestas y comentarios:
Comparemos 3 opciones diferentes para una biblioteca, que deseen presentar un Singleton global, como una variable o mediante una función de obtención:
Opción 1 : el patrón de contador ingenioso , que permite el uso de una variable global que es:
- Asegurado para ser creado una vez
- Asegurado para ser creado antes del primer uso
- se aseguró que se crearía una vez en todos los objetos compartidos que se vinculan dinámicamente con la biblioteca que crea esta variable global .
Opción 2 : el patrón singleton de Meyers con una variable de referencia (como se presenta en la pregunta):
- Asegurado para ser creado una vez
- Asegurado para ser creado antes del primer uso
Sin embargo, creará una copia del objeto singleton en objetos compartidos, incluso si todos los objetos compartidos y el objeto principal están vinculados dinámicamente con la biblioteca. Esto se debe a que la variable de referencia Singleton se declara estática en un archivo de encabezado y debe tener su inicialización lista en el momento de la compilación donde sea que se use, incluso en los objetos compartidos, durante el tiempo de compilación, antes de cumplir con el programa en el que se cargarán.
Opción 3 : el patrón Singleton de Meyers sin una variable de referencia (que llama a un captador para recuperar el objeto Singleton):
- Asegurado para ser creado una vez
- Asegurado para ser creado antes del primer uso
- se aseguró que se crearía una vez en todos los objetos compartidos que se vinculan dinámicamente con la biblioteca que crea este Singleton .
Sin embargo, en esta opción no hay una variable global ni una llamada en línea, cada llamada para recuperar el Singleton es una llamada de función (que se puede almacenar en caché en el lado del llamante).
Esta opción se vería como:
// libA .h
struct A {
A();
};
A& getA();
// some other header
A global_a2 = getA();
// main
int main() {
std::cerr << "main/n";
}
// libA .cpp - need to be dynamically linked! (same as libstdc++ is...)
// thus the below shall be created only once in the process
A& getA() {
static A a;
return a;
}
A::A() { std::cerr << "construct A/n"; }
Todas sus preguntas sobre la utilidad / rendimiento de Nifty Counter, también conocido como Schwartz Counter, fueron respondidas básicamente por Maxim Egorushkin en esta respuesta (pero también consulte los subprocesos de comentarios).
Variables globales en C ++ moderno
El problema principal es que se está produciendo una compensación. Cuando usa Nifty Counter, el tiempo de inicio de su programa es un poco más lento (en proyectos grandes), ya que todos estos contadores tienen que ejecutarse antes de que pueda pasar cualquier cosa. Eso no sucede en el singleton de Meyer.
Sin embargo, en el singleton de Meyer, cada vez que quiera acceder al objeto global, debe verificar si es nulo, o el compilador emite un código que verifica si la variable estática ya se construyó antes de intentar el acceso. En el contador de Nifty, ya tienes tu puntero y simplemente disparas, ya que puedes asumir que el inicio ocurrió en el momento del inicio.
Por lo tanto, Nifty Counter vs Meyer singleton es básicamente una compensación entre el tiempo de inicio del programa y el tiempo de ejecución.