Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 55 di 93
  • livello avanzato
Indice lezioni

Il qualificatore static

In C++, la parola chiave static assume un significato diverso a secondo di quando e come viene usata: ecco un approfondimento completo sull'argomento.
In C++, la parola chiave static assume un significato diverso a secondo di quando e come viene usata: ecco un approfondimento completo sull'argomento.
Link copiato negli appunti

La parola chiave static in C++ ricorre in numerosi contesti e con svariate declinazioni. Tuttavia i vari ambiti di utilizzo condividono tutti un terreno comune: l'aggettivo "statico", in C++, è riferito a qualcosa che viene risolto in fase di compilazione. Il senso di questa affermazione risulterà più chiaro in seguito, quando analizzeremo l'effetto di questo qualificatore da solo e in congiunzione o opposizione ad altri.

Variabili statiche

Il primo concetto fondamentale legato al termine static in C++ è quello inerente lo storage, inteso come dove e per quanto una variabile è accessibile in uno specifico ambito di visibilità della nostra applicazione.

Per dove si intende il segmento di memoria che ospita una variabile durante l'esecuzione del nostro programma. Per una variabile statica, il segmento predefinito varia a seconda che essa sia inizializzata o meno contestualmente alla sua dichiarazione. Generalmente una variabile statica non inizializzata risiede di norma nel segmento BSS, mentre quelle inizializzate risiedono nel segmento Data. Tuttavia, questa ripartizione può variate in base al compilatore in uso.

Lo spazio di memoria destinato ad ospitare le variabili statiche sia globali che locali viene riservato già a tempo di compilazione, tuttavia sussiste una differenza fondamentale che riguarda la fase di inizializzazione, cioè l'attribuzione di un valore.

In particolare, una variabile statica globale viene inizializzata sempre contestualmente al caricamento del programma, eventualmente a zero nel caso in cui non le sia assegnato un valore. Una variabile statica locale viene invece inizializzata solo la prima volta che il controllo di flusso entra nel suo ambito di visibilità. L'esempio seguente mostra una variabile statica localmente definita in una funzione che, dopo la prima inizializzazione, ne incrementa il valore e lo stampa a schermo.

#include <iostream>
void funcWithStaticVar()
{
// istruzione eseguita solo la prima volta che la funzione viene invocata
static int i = 1;
std::cout << "i = " << ++i << "\n";
}
int main()
{
funcWithStaticVar(); // stampa 2
funcWithStaticVar(); // stampa 3
return 0;
}

Una volta passata la fase di inizializzazione, una variabile statica globale o locale persiste fino al termine dell'esecuzione del programma.

Nell'esempio precedente, poiché la variabile i sopravvive anche al di fuori del suo contesto di visibilità, successive invocazioni della funzione produrranno un incremento del suo valore. Tuttavia, la sua località ne limita l'accesso al solo contesto nativo che è quello corrispondente al corpo della funzione funcWithStaticVar().

Linkage delle variabili statiche

Il concetto di linkage che abbiamo introdotto nella descrizione del qualificatore const, si applica allo stesso modo nel caso di variabili statiche. Una variabile statica globale ha quindi linkage interno per default, mentre una variabile statica locale non ha linkage, essendo confinata al solo ambito di visibilità nativo.

Una variabile globale non qualificata static ha un comportamento ibrido, in quanto la sua locazione di memoria e la sua durata sono definite allo stesso modo di una variabile statica, ma ha linkage esterno per default, cioè è visibile anche all'esterno dell'unità di traduzione in cui è dichiarata.

In questo senso, static può essere usato per modificare il linkage di una variabile globale. Un modo alternativo che non richiede l'uso del qualificatore static consiste nel confinare una variabile globale in un namespace anonimo, come mostrato nel frammento seguente:

int y; // variabile globale con linkage esterno e durata statica.
static int x; // variabile globale con linkage interno e durata statica.
namespace {
int x; // variabile con visibilità limitata al namespace anonimo che la contiene,
// il linkage è interno, con riferimento al blocco delimitato dal namespace anonimo.
}

static vs const

Sebbene nel caso generale static e const abbiano una semantica differente, per il solo caso delle variabili definite in ambiente globale, o all'interno di un namespace, l'uso di const sussume implicitamente quello di static.

// variabile globale read-only con linkage interno (const)
// e durata statica (implicitamente static).
const int x = 0;

Pertanto nel frammento precedente, la variabile globale x, pur non essendo qualificata come statica, ha linkage interno oltre che essere accessibile in sola lettura.

Dichiarazione e inizializzazione di dati membro statici

Anche i dati membro ed i metodi di una classe possono essere definiti come statici. In questo contesto il qualificatore static denota delle entità che sono uniche per classe e condivise da tutte le istanze.

Come regola generale, la dichiarazione dei dati membro statici di una classe, è scissa dalla loro definizione che è, di norma, inclusa nel file di implementazione dei metodi della classe stessa (.cpp). Nell'esempio seguente, la dichiarazione della classe MyDummyClass include un membro statico di tipo intero nella sezione pubblica:

// mydummyclass.h
#ifndef MY_DUMMY_CLASS
#define MY_DUMMY_CLASS
namespace dummy {
class MyDummyClass
{
public:
MyDummyClass() {};
~MyDummyClass() {};
public:
static int myDummyNumber; // Nota bene: dichiarazione senza definizione
};
} // namespace dummy
#endif // MY_DUMMY_CLASS

Il corrispondende file di implementazione conterrà la definizione di myDummyNumber come segue:

// mydummyclass.cpp
#include "mydummyclass.h"
namespace dummy {
// definizione
int MyDummyClass::myDummyNumber = 40;
} // namespace dummy

Si noti che la definizione di un membro statico include il tipo e la risoluzione esplicita dello scope della classe (MyDummyClass::). Il qualificatore static non è invece ripetuto nella definizione, anzi questo comporterebbe un errore di compilazione.

Si noti anche che il modificatore di accesso (public, protected o private) usato per la dichiarazione del membro statico non-const è trasparente rispetto alla definizione dello stesso, cioè non altera la sintassi di inizializzazione del dato membro nel file di implementazione.

Infine, per completezza, in questo esempio abbiamo confinato la classe entro un namespace. Come regola generale, la definizione di un dato membro statico deve avere lo stesso ambito di visibilità della dichiarazione della classe, cioè lo spazio globale o un namespace, come appunto in questo caso.

Nel listato seguente il dato membro statico myDummyNumber viene acceduto, modificato e stampato a schermo. Si noti che essendo un membro statico, esso non appartiene ad una istanza della classe, ed in effetti, nessuna oggetto della classe MyDummyClass viene mai istanziato in questo esempio.

#include <iostream>
#include "mydummyclass.h"
int main()
{
using namespace dummy;
MyDummyClass::myDummyNumber += 2;
std::cout << "La risposta è " << MyDummyClass::myDummyNumber << "!\n";
return 0;
}

Ovviamente, nulla vieta di accedere ad un membro statico tramite un'istanza specifica della classe, tramite l'operatore . o ->, compatibilimente con il suo modificatore di accesso.

Il qualificatore static e la One Definition Rule (ODR)

In generale, lo standard C++ proibisce la definizione di un dato membro statico contestualmente alla sua dichiarazione. Tale limitazione è connessa al modo in cui avviene il linking dei simboli contenuti in ogni unità di traduzione. La definizione di un membro di classe statico all'interno della dichiarazione della classe, comporterebbe di norma la ridefinizione dello stesso simbolo in ogni unità di traduzione in cui il file header della classe venga incluso, complicando di molto il lavoro del linker.

Tale circostanza è infatti riconducibile ad una violazione della one definition rule o ODR che, in termini estremamente concisi, sancisce il fatto che ogni entità (variabile, funzione o classe) sia univocamente definita in un programma, e ciò vale anche per i dati membri statici ovviamente.

Unica eccezione concessa alla regola generale riguarda i membri dati statici e accessibili in sola lettura, come mostrato nella versione modificata della classe MyDummyClass:

// mydummyclass.h
#ifndef MY_DUMMY_CLASS
#define MY_DUMMY_CLASS
namespace dummy {
class MyDummyClass
{
public:
MyDummyClass() {};
~MyDummyClass() {};
public:
static int myDummyNumber;
static const char myDummyChar = 'C'; // dichiarazione e definizione
};
} // namespace dummy

In questo caso il compilatore si riserva la possibilità di effettuare l'inlining del dato membro statico e read-only, estinguendo possibili ambiguità a livello di linking.

Tuttavia, questa soluzione genera problemi quando il simbolo in questione è acceduto per riferimento o il suo valore è modificato. Lo standard definisce questa fattispecie di simboli odr-used, cioè entità univocamente definite che hanno un nome e una locazione di memoria associata.

Quindi se abbiamo definito un membro statico read-only all'interno della nostra classe, aspettiamoci problemi di linking ogni qualvolta si provi ad accedere al suo indirizzo. Come ad esempio nel listato seguente, che fa riferimento alla nuova versione della classe.

#include <iostream>
#include "mydummyclass.h"
int main()
{
using namespace dummy;
std::cout << "Ecco a voi la lettera " << MyDummyClass::myDummyChar << "!\n";
// odr-use di myDummyChar, genererà un errore di linking
const char* ptr = &(MyDummyClass::myDummyChar);
return 0;
}

La soluzione consiste nel muovere la definizione del membro statico costante al di fuori della classe, applicando quindi la regola generale.

Metodi statici e interazioni con altri qualificatori

Abbiamo già discusso della definizione di metodi statici, e delle implicazioni dell'uso di questo qualificatore, come ad esempio l'incompatibilità con il qualificatore const.

La causa di tale ortogonalità è riconducibile alle modalità in cui l'uso del puntatore this altera la firma di un metodo in C++, oltre che ad un'evidente contraddizione in termini semantici.

La stessa idiosincrasia è evidente nel caso del qualificatore virtual, che trova uso nell'associazione dinamica (cioè a tempo di esecuzione) di metodi a oggetti polimorfici.

Dovrebbe essere infatti palese che un metodo statico, cioè slegato dalle singole istanze di una classe, sia incompatibile con il concetto di polimorfismo che si basa invece sulla selezione del comportamento idoneo in base alla particolare istanza che lo invoca.

Sebbene quindi da un punto di vista semantico, l'uso di tali qualificatori non può che essere mutuamente esclusivo, esistono delle circostanze in cui, per ragioni meramente implementative, questa incompatibilità risulta di ostacolo. Sotto quest'ottica, analizzeremo uno schema di programmazione ricorrente, cioè un costrutto idiomatico in C++, che concilia l'uso dei qualificatori virtual e static per soddisfare alcune esigenze specifiche di programmazione.

Virtual-Static Idiom

Virtual-Static è una tecnica molto semplice che consente di incapsulare l'invocazione di un metodo virtuale all'interno di un metodo statico al quale viene passato come argomento un puntatore ad un'istanza della classe. Tale argomento svolge la medesima funzione del puntatore this, ma in forma esplicita. Il listato seguente mostra un esempio dell'applicazione di questo schema:

#include <iostream>
class Base
{
public:
static void staticFunction(Base* b)
{
b->function();
}
virtual void function()
{
std::cout << "Base method!" << "\n";
}
};
class Derived : public Base
{
public:
virtual void function() override
{
std::cout << "Derived method override!" << "\n";
}
};
int main()
{
Base* d = new Derived();
Base::staticFunction(d); // stampa "Derived method override!"
return 0;
}

La classe Base definisce un metodo statico che invoca un metodo virtuale della stessa classe applicato al puntatore passato come argomento. Per effetto dell'associazione dinamica del metodo con l'istanza chiamante, il risultato prodotto è l'invocazione del corretto sovraccarico del metodo virtuale sull'istanza d, il cui tipo è un derivato della classe base.

Questo idioma trova solitamente applicazione in tutti quei casi in cui è necessario integrare un data model costituito da classi in C++ con librerie o chiamate a funzioni di sistema che usano una sintassi in stile C per l'invocazione di callback.

Per via della sua particolare semantica, il qualificatore static applicato ad un metodo di classe esclude dalla firma del metodo il parametro implicito this. Ciò di fatto rende la dichiarazione di un metodo statico del tutto simile a quella di una funzione esterna.

In molte librerie scritte in linguaggio C la definizione dei callback è tipicamente una funzione priva di un valore di ritorno e avente un unico parametro di tipo void* che consente al programmatore di incapsulare tutti i dati necessari all'esecuzione del callback stesso in un'entità opaca, cioè immune al meccanismo di controllo statico dei tipi. Esso viene infatti demandato all'utilizzatore, che si assume l'onere di gestirla correttamente.

Il vantaggio nell'uso di questa tecnica consiste quindi nel poter usare i metodi di una classe come callback in stile C, preservandone l'eventuale natura polimorfica se essi sono virtuali. Inoltre, poiché si rimane all'interno dello scope della classe stessa, vengono aggirate le limitazioni che si avrebbero con l'uso di una funzione esterna per l'accesso a membri protetti e privati.

Ti consigliamo anche