Spróbowałem sobie "zamodelować" sposób powstawania obiektu std::cout. Wyczytałem, że wykorzystywany jest do tego celu idiom C++ znany jako "Schwarz Counter" (albo "Nifty Counter").
(Nazwy w kodzie odnoszące się do oryginalnych z STL-a opatrzyłem sufiksem "_m").
ios_m.h:
#ifndef IOS_M_H
#define IOS_M_H
class ios_base_m
{
public:
//...
class Init_m
{
public:
Init_m();
~Init_m();
static int nifty_counter;
};
};
#endif // IOS_M_H
ios_m.cpp:
#include <new> // placement new
#include <type_traits> // aligned_storage
#include "ostream_m.h"
int ios_base_m::Init_m::nifty_counter;
static typename std::aligned_storage<sizeof (ostream_m), alignof (ostream_m)>::type stream_buf; // memory for the stream object
ostream_m& cout_m = reinterpret_cast<ostream_m&> (stream_buf);
//...
ios_base_m::Init_m::Init_m()
{
if (nifty_counter++ == 0) new (&cout_m) ostream_m(); // placement new
}
ios_base_m::Init_m::~Init_m()
{
if (--nifty_counter == 0) (&cout_m)->~ostream_m();
}
ostream_m.h:
#ifndef OSTREAM_M_H
#define OSTREAM_M_H
#include "ios_m.h"
class ostream_m : public ios_base_m
{
public:
ostream_m();
~ostream_m();
int flag;
//...
};
#endif // OSTREAM_M_H
ostream_m.cpp:
#include "ostream_m.h"
ostream_m::ostream_m()
{
flag = 5;
//...
}
ostream_m::~ostream_m()
{
flag = 0;
//...
}
//...
iostream_m.h:
#ifndef IOSTREAM_M_H
#define IOSTREAM_M_H
#include "ostream_m.h"
extern ostream_m& cout_m;
static ios_base_m::Init_m init_m;
#endif // IOSTREAM_M_H
main:
#include <iostream>
#include "iostream_m.h"
using namespace std;
int main()
{
cout<<cout_m.flag<<endl;
cout_m.flag = 20;
cout<<cout_m.flag<<endl;
return 0;
}
Oczywiście to tylko model, ale jak sądzicie - czy prawidłowy?
Podsumowanie:
Zastanawiałem się tu w jaki sposób powstaje obiekt std::cout, po co bazowej klasie tego obiektu potrzebna jest klasa wewnętrzna ios_base::Init oraz jaką rolę w tym wszystkim odgrywa idiom „Nifty Counter” (albo: „Schwarz Counter”). Chyba ogarnąłem temat :)
Otóż:
Załóżmy, że w naszym projekcie oprócz pliku z main() mamy jeszcze dwie jednostki translacji: A i B. Załóżmy też, że każda z nich tworzy jedną zmienną globalną o nietrywialnej inicjalizacji (tzn. posiadającą określony konstruktor). Okazuje się, że kolejność kompilacji tych jednostek NIE jest ustalona, co oznacza, że kolejność inicjalizacji tych zmiennych nie jest ustalona.
Załóżmy teraz, że kompilacja jednostki A wyprzedza kompilację jednostki B. Jeżeli konstruktor zmiennej globalnej z jednostki A odwoływał się jakoś do zmiennej globalnej z jednostki B to mamy problem: zmienna z jednostki B nie została jeszcze zainicjalizowana. Inny przykład problemu: konstruktor zmiennej z jednostki A pisze coś do pliku, który otwiera konstruktor zmiennej z jednostki B.
Dlatego właśnie na początku każdej jednostki translacji odwołującej się do obiektu std::cout włączamy plik <iostream>. Plik ten zawiera deklarację zewnętrznego obiektu cout oraz tworzy obiekt klasy ios_base::Init. Konstruktor klasy ios_base::Init sprawdza i postinkrementuje licznik – pole statyczne tej klasy (ten licznik to właśnie „Schwarz Counter”). Jeżeli licznik równy jest zero to konstruktor inicjalizuje obiekt cout.
To zapewnia, że:
1). obiekt cout jest inicjalizowany już podczas kompilacji pierwszej jednostki translacji zawierającej plik <iostream> (i tylko wtedy);
2). obiekt cout jest inicjalizowany PRZED inicjalizacją jakichkolwiek zmiennych globalnych/statycznych występujących w tych jednostkach. I gdyby ich konstruktory odwoływały się do obiektu cout to odwołują się już do zainicjalizowanego obiektu cout.
Tak to chyba jakoś wygląda :)