Creazione di oggetti

27 marzo 2017

Nelle lezioni precedenti abbiamo introdotto gli strumenti sintattici per la creazione di oggetti: i costruttori e gli operatori di assegnamento che servono a inizializzare nuove istanze a partire da oggetti già esistenti.

Questa lezione ha lo scopo di completare la presentazione di questi costrutti fornendo alcuni esempi sul loro utilizzo ed analizzando alcuni degli errori più comuni.

L’esempio seguente presenta vari casi di inizializzazione di oggetti della classe Point2D che fanno uso dei costruttori e degli operatori di assegnamento, di copia e spostamento:

#include "point2d.h"

using Geometry::Point2D;

int main()
{
	// invoca il costruttore Point2D(double, double)
	Point2D a(1, 1);
	Point2D b(2, 2);

	// costruttore di copia
	Point2D c = a;

	// operatore di assegnamento di copia
	a = b;

	// costruttore di spostamento
	Point2D d = std::move(Point2D(3, 3));

	// operatore di assegnamento di spostamento
	a = Point2D(4, 4);

	return 0;
}

Si noti in questo caso che per inizializzare d si rende necessario l’uso di std::move per evitare che, per effetto dell’ottimizzazione del codice da parte del compilatore, avvenga un’elisione di copia piuttosto che l’invocazione del costruttore di spostamento.

Questo caso particolare ha soli fini didattici; normalmente non dobbiamo preoccuparci di usare std::move per l’inizializzazione di un oggetto, anzi è molto meglio lasciare al compilatore l’onere dell’ottimizzazione. L’uso del costruttore di spostamento è quasi sempre trasparente agli occhi del programmatore, ad esempio quando si passano valori temporanei come argomenti di funzioni o quando viene restituito il valore di ritorno. Inoltre, l’uso esplicito di std::move potrebbe degradare le prestazioni del codice se applicato impropriamente.

Puntatori a oggetti e allocazione dinamica

Le istanze di oggetti di tipo composto possono essere allocate dinamicamente mediante l’uso dei puntatori e delle parole riservate new e delete analogamente a quanto visto finora per le variabili di tipo primitivo. L’esempio seguente mostra l’allocazione dinamica di oggetti di tipo Point2D:

#include "point2d.h"

using Geometry::Point2D;

int main()
{
	Point2D *p1 = new Point2D(1, 2);
	Point2D *p2 = new Point();	// invoca il costruttore di default
	Point2D *p3 = new Point;	// niente parentesi

	Point2D p4(3, 4);

	p1->setX(5);	// ->
	p4.setX(6);		// .

	delete p1;		// deallocazione di p1

	return 0;
}

Le variabili p1,p2 e p3 sono esempi di inizializzazione dinamica. La parole chiave new è seguita dal costruttore completo di una lista di argomenti tra parentesi tonde. Nel caso del costruttore di default privo di argomenti, è possibile lasciare le parentesi tonde vuote (p2) o ometterle del tutto (p3). Le due sintassi in questo caso sono equivalenti, tuttavia ciò non sempre è vero. La differenza verrà chiarita nei paragrafi successivi.

È opportuno inoltre notare che la modalità di allocazione condiziona la sintassi che viene usata per l’accesso ai membri di classe. In particolare, per istanze allocate dinamicamente (puntatori a oggetti) è richiesto l’uso dell’operatore ->, mentre per le istanze allocate staticamente si usa l’operatore punto ., come mostrato per l’invocazione della funzione membro setX per p1 e p4 rispettivamente.

Most Vexing Parse

Questo paragrafo è dedicato ad alcuni degli errori sintattici più ricorrenti nell’inizializzazione statica di oggetti, dovuti all’applicazione da parte del compilatore di alcune regole di parsing che risultano contro-intuitive per noi programmatori, ma che, tuttavia, hanno una loro ragione d’essere.

Il primo caso che prendiamo in esame riguarda il modo corretto di invocare il costruttore di defaut, ed è illustrato nel frammento seguente:

Point2D good;	// dichiarazione di variabile
Point2D bad(); 	// dichiarazione di funzione

La prima istruzione viene correttamente interpretata dal compilatore come la dichiarazione di una variabile di tipo Point2D di nome good. Tuttavia quella successiva, che molto spesso viene usata con l’intento di invocare esplicitamente il costruttore di default, in realtà viene interpretata dal compilatore come una dichiarazione anticipata di funzione.

Nel caso specifico, una funzione di nome bad priva di argomenti che restituisce un oggetto di tipo Point2D.

Tale istruzione in sè non genera un errore di compilazione, tuttavia lo farà ogni tentativo di accesso ai membri di classe dell’istanza di nome bad. La soluzione in questo caso consiste banalmente nel rimuovere le parentesi tonde che seguono l’identificatore della variabile.

Tuttavia non abbiamo ancora esaurito le complicazioni da esaminare; il secondo caso che prendiamo in esame riguarda l’impiego di costruttori che hanno per argomenti const reference. Non ci limitiamo in senso stretto ai costruttori di copia, ma anche a costruttori generici che hanno per argomento una const reference ad un oggetto di tipo differente da quello della classe di appartenenza, come mostrato nell’esempio seguente:

#include "point2d.h"

using Geometry::Point2D;

class Circle
{
	public:
	Circle(const Point2D& c)
	{
			center = c;
			radius = 1.0f;
	}

	float getRadius()
	{
		return radius;
	}

	private:
	Point2D center;
	float radius;
};

int main()
{
	Point2D p(Point2D()); 	// most vexing parse
	Circle c1(Point2D()); 	// most vexing parse

	p.setX(3);				// errore
	c1.getRadius();			// errore

	return 0;
}

Anche in questo caso, mentre la maggior parte dei programmatori si aspetta che le prime due istruzioni della funzione main siano interpretate come inizializzazioni, in realtà il compilatore le interpreta come dichiarazioni anticipate di funzione, generando errori di compilazione alle righe successive.

La prima istruzione viene infatti interpretata come la dichiarazione di una funzione di nome p che restituisce un oggetto di tipo Point2D e che ha per argomento un puntatore a funzione che restituisce a sua volta un oggetto di tipo Point2D e non ha argomenti. Molti programmatori si aspetterebbero invece la dichiarazione di una variabile di tipo Point2D inizializzata mediante il costruttore di copia applicato ad un temporaneo della stessa classe.

La seconda viene invece interpretata come la dichiarazione di una funzione di nome c1 che restituisce un oggetto di tipo Circle e che accetta come argomento un puntatore a funzione come quello descritto in precedenza. Anche in questo caso, l’interpretazione più probabile per un programmatore sarebbe quella della dichiarazione di una variabile di tipo Circle.

Questo secondo caso è noto come most vexing parse, che possiamo tradurre alla buona con la perifrasi “la regola di parsing più stringente” (o irritante, se lo si preferisce).

Il motivo per cui il compilatore applica questa regola è ancora una volta da ricercarsi nella retro-compatibilità con il linguaggio C in cui non esistono né classi né costruttori. Pertanto il compilatore privilegia sempre la semantica della dichiarazione di funzione a scapito di quella dell’inizializzazione di un oggetto.

La soluzione consiste in questo caso, nell’applicare un paio di parentesi tonde agli argomenti del costruttore, o nel ricorrere alla sintassi di inizializzazione uniforme introdotta dallo standard c++11, di cui parleremo nelle lezioni successive.

Modalità di inizializzazione

Per concludere la panoramica sulle modalità di creazione di oggetti, in questo paragrafo analizzeremo alcuni degli effetti che derivano dalla presenza o meno nelle nostre classi del costruttore di default, sia esso definito dal programmatore o generato automaticamente dal compilatore.

Quando non definiamo esplicitamente un costruttore di default, l’istanziazione di una classe i cui dati membri sono di natura eterogenea (tipi primitivi, composti, puntatori o reference eccetera) può avere esiti differenti a seconda della modalità di inizializzazione usata.

L’inizializzazione dipende infatti dall’ambito di visibilità delle variabili, dal fatto che esse siano di tipo primitivo o composto, dai qualificatori applicati ai membri di classe, dalla sintassi usata per l’inizializzazione dell’oggetto ed infine dallo standard di riferimento che usiamo per la compilazione. Un esempio di questo fenomeno è osservabile nel listato seguente:

#include <iostream>

using namespace std;

class A {
public:
	int m;
	int *ptr;
};

A globalVar;

int main()
{
	A localVar1;
	A localVar2 = A();

	cout << "localVar1.m ha un valore indeterminato: " << localVar1.m << endl;
	cout << "localVar2.m è zero: " << localVar2.m << endl;

	delete globalVar.ptr;   // ok
	cout << "Deallocazione di globalVar.ptr" << endl;

	delete localVar2.ptr;   // ok
	cout << "Deallocazione di localVar2.ptr" << endl;

	delete localVar1.ptr;   // errore di segmentazione!
	cout << "questa stringa non verrà stampata!" << endl;

	return 0;
}

La classe A contiene soltato due membri pubblici, un intero ed un puntatore a intero, ma non contiene la definizione di alcun costruttore. Le tre istanze di A usate nel listato, globalVar, localVar1 e localVar2, si trovano in contesti differenti, e le ultime due sono inizializzate con una sintassi diversa.

Queste differenze sono significative ai fini della inizializzazione dei dati membro delle istanze di A. Infatti, quelli di globalVar e localVar2 saranno inizializzati a 0 e nullptr, mentre quelli di localVar1 avranno valori indeterminati.

Ciò è la causa del diverso effetto prodotto delle deallocazione dei puntatori contenuti in globalVar e localVar2, che è sicura, e di quella del puntatore contenuto in localVar1 che invece causerà la terminazione immediata del programma.

Perchè? Il compilatore genera un costruttore di default, e tuttavia, tutte è tre le variabili sono inizializzate in maniera differente:

  • globalVar è inizializzata a zero (zero initialization): il costruttore di default non viene chiamato, ma tutti i tipi primitivi sono inizializzati a zero.
  • per localVar1 è usata la inizializzazione di default (default initialization): viene invocato il costruttore di default.
  • per localVar2 è usata la inizializzazione con valore (cioè un temporaneo) (value initialization): viene invocato il costruttore di default, ed in seguito tutti i membri di tipo primitivo sono inizializzati a zero, ma solo perchè in questo caso il costruttore è stato generato automaticamente dal compilatore.

Le regole sintattiche che distinguono una modalità di inizializzazione dall’altra sono anche esse articolate: in generale quando il costruttore di default viene invocato senza le parentesi “()”, avviene una inizializzazione di default, altrimenti è per valore, ma questa sottile differenza si applica solo a partire dallo standard c++11. Una variabile globale non inizializzata con alcun valore invece è sempre nulla semplicemente perchè è allocata nel segmento di memoria BSS che è sempre inizializzato a zero.

Per le due variabili locali, le cose cambiano se nella classe A introduciamo esplicitamente un costruttore di default. In questo caso, l’esito dipende dalla particolare implementazione: se il costruttore è vuoto, anche localVar2 avrà valori indeterminati, altrimenti avrà i valori che gli vengono assegnati dal costruttore.

La variabilità cui è soggetta l’inizializzazione degli oggetti in C++ è tale che non vale la pena di imparare a memoria le regole in base alle quali viene applicata una modalità di inizializzazione o l’altra.

Piuttosto, al fine di essere sicuri di inizializzare sempre in maniera coerente, è più facile ed opportuno implementare il costruttore di default avendo cura di inizializzare tutti i membri della classe con un valore che abbia senso per il nostro modello concettuale. In questo modo non solo siamo in pieno controllo di ciò che avviene in fase di inizializzazione, ma siamo anche sicuri che chiunque usi le nostre classi ignaro della loro implementazione, osserverà sempre lo stesso comportamento, indipendentemente dalla sintassi usata per istanziare degli oggetti.

Tutte le lezioni

1 ... 19 20 21 ... 60

Se vuoi aggiornamenti su Creazione di oggetti inserisci la tua e-mail nel box qui sotto:
Tags:
 
X
Se vuoi aggiornamenti su Creazione di oggetti

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento di cui al punto 3 dell'informativa sulla privacy