Past na konstruktor - static v C++

Past na konstruktor - static v C++

Autor: progrosh
Občas zahlédnu v našem kódu statické členské proměnné, ikdyž se jim snažíme vyhýbat, jak se dá. Někdy se bez static zkrátka neobejdeme, ale právě proto bychom měli mít na paměti i rizika, která jsou s použitím tohoto způsobu alokace spojená. U statických členských proměnných se totiž snadno může stát, že se jejich konstruktory vůbec nezavolají v pořadí, jak bychom očekávali. A pozor! V C++ to není chyba kompilátoru nebo linkeru, ale vlastnost!

Ukážeme si to na malém příkladu. Začneme tím nejjednodušším programem v C++:

int main()
{
 return 0;
}

Program nebude dělat nic. Pouze vrátí 0 jako svůj exit code. Náš článek je o static a konstruktorech, tak se vše bude odehrávat v konstruktorech tříd a staticky alokovaných členských proměnných tříd, které se chovají podobně, jako globální proměnné.
Vytvoříme si 3 jednoduché třídy A, B a C.

// @file testA.h
#ifndef A_H_
#define A_H_

#include <iostream>

class A
{
  int x;
public:
  A() : x(1) {
    std::cout << "A's constructor called x=" << x << std::endl;
  }
  int get() const {
    return x;
  }
};

#endif
// @file testB.h
#ifndef B_H_
#define B_H_
#include "testA.h"

class B
{
 static A a;
public:
 B();
 static A getA();
};

#endif
// @file testC.h
#ifndef C_H_
#define C_H_
#include "testB.h"

class C
{
 static B b;
public:
 C();
 static B getB();
};

#endif

A protože třídy B a C používají static členskou proměnnou, musíme tyto členské proměnné definovat v implementaci. Vytvoříme proto pro ně 2 samostatné soubory testB.cc a testC.cc.

// @file testB.cc
#include <iostream>
#include "testB.h"
using namespace std;

A B::a;

B::B() {
  cout << "B's constructor called a.get()=" << a.get() << endl;
}

A B::getA() {
  return a;
}
// @file testC.cc
#include <iostream>
#include "testC.h"
using namespace std;

B C::b;

C::C() {
  cout << "C's constructor called " << endl;
}

B C::getB() {
  return b;
}

Třída A má obyčejnou členskou proměnnou x typu int. Její konstruktor zajišťuje, aby členskou proměnnou x vždy inicioval na hodnotu 1. Pod tímto si můžete představit jakoukoliv jinou složitější třídu, která bude například v konstruktoru vytvářet a inicializovat složitější struktury nebo dynamicky alokovat paměť.

A nyní přijde to nejzajímavější - kompilace a spuštění našeho programu.
Kompilace:

g++ --std=c++11 test.cc testB.cc testC.cc -o test

Spuštění:

./test
A's constructor called x=1
B's constructor called a.get()=1

Je vidět, že vše je v pořádku. Program se spustil a jelikož obsahoval 2 třídy se statickými členskými proměnnými, zavolal jejich konstruktory a vytvořil instance požadovaných tříd. Na rozdíl od funkcí, statické členské proměnné třídy se vytváří automaticky ještě před vytvořením instance třídy, která statickou členskou proměnnou obsahuje. Proto náš hlavní program může být prázdný a konstruktory statických členských proměnných se zavolají.
Nyní program zkompilujeme takto:

g++ --std=c++11 test.cc testC.cc testB.cc -o test

Spuštění:

./test 
B's constructor called a.get()=0
A's constructor called x=1

A ejhle! Konstruktor třídy B se zavolal před konstruktorem třídy A, přestože program jasně definuje závislost třídy B na třídě A. Třída B totiž obsahuje statickou instanci třídy A, proto bychom předpokládali, že to kompilátor pozná a nastaví správné pořadí volání konstruktorů podle implementace. Nestane se tak, protože hlavní slovo v pořadí volání konstruktorů staticky alokovaných instancí má linker. A protože jsme komponenty programu linkovali v pořadí testC.cc a následně až testB.cc, pořadí volání konstruktorů statických členských proměnných se nastavilo podle pořadí, v jakém je našel linker. Ten už bohužel závislosti nezkoumá a u statických instancí pořadí určí podle toho, v jakém mu jsou předloženy.

Proto pokud používáte globální statické proměnné, dejte si pozor na to, v jakém pořadí budete komponenty programu linkovat nebo si musíte zajistit pořadí volání konstruktorů statických instancí sami v implementaci explicitním voláním jejich konstruktoru, kde si jednotlivé závislosti a pořadí volání konstruktorů můžete ošetřit sami. Tomuto postupu se říká “nifty counter”.

Nejjednodušší způsob, jak tomuto nežádoucímu chování zabránit, je statické členské proměnné vůbec nepoužívat nebo použít maximálně jednu v celé aplikaci. A tím obvykle bývá singleton třídy aplikace.

Komentáře