Dal C al C++/Utilizzo avanzato di librerie/I parametri di default

I valori di default per i parametri nelle chiamate di funzioneModifica

MotivazioneModifica

Una libreria, in un primo tempo, contiene una funzione di grande complessità, dichiarata nel seguente modo:

int f(double x);

Il codice applicativo contiene, in numerosi punti, istruzioni come la seguente:

int a = f(2.7) * 2;

Tale funzione usa internamente un valore costante pari a 4. Un giorno si decide che, per un caso particolare, quella costante interna deve valere 5.

Per evitare di duplicare l'intera funzione, si può trasformate tale costante in un parametro, aggiunto alla lista di parametri della funzione, dichiarandola nel seguente modo:

int f(double x, int n);

Nel punto in cui serviva tale funzionalità, la funzione è chiamata con la seguente istruzione:

int b = f(y, 5);

Tale modifica ha però un inconveniente: in tutti i molteplici punti in cui la vecchia funzione veniva utilizzata, si ha un errore sintattico, in quanto la funzione viene chiamata passando un solo parametro, quando orami se ne aspetta due. Una modifica di questo tipo a una funzione di libreria, se usata in numerosi programmi, viene detta in inglese "breaking change", cioè "modifica dirompente", in quanto rende illegale o scorretto del codice che prima era legale e corretto.

In linguaggio C, l'unico modo di evitare una modifica dirompente e di duplicare del codice è procedere nel seguente modo. La funzione in cui la costante è diventata un parametro cambia di nome, per esempio dichiarandola nel seguente modo:

int f2(double x, int n);

Si crea la seguente funzione:

int f(double x) { return f2(x, 4); }

In tal modo, tutti i vecchi utilizzatori di "f", senza saperlo, chiamano la nuova funzione "f2", passandole come parametro il valore che prima era una costante. Gli utilizzatori che volessero utilizzare un valore diverso, possono chiamare direttamente "f2".

In C++, per semplificare tale problematica, è stato introdotto il concetto di "valore di default" per i parametri di funzione. Basta trasformare la vecchia dichiarazione della funzione "f" nella seguente dichiarazione:

int f(double x, int n = 4);

Questa dichiarazione permette di chiamare "f" sia con due parametri che con un parametro solo. Chiamandola con un solo parametro, il parametro mancante assumerà il valore indicato dopo il segno "=".

RegoleModifica

Il valore di default può essere un'espressione che possa essere usata per inizializzare una variabile del tipo dichiarato. Per esempio, la seguente dichiarazione di "f1" è valida, mentre quella di "f2" non lo è:

int a = 3;
int f1(double x, int n = 4 * a);
int f2(int n1, int n2 = "due"); // Illegale

Si possono dichiarare valori di default per più parametri, anche tutti. Ecco un esempio di dichiarazione e di chiamate valide:

void f(int a = 3, char b = 'a', bool c = true, double d = 1.0);
f(7, 'q', false, 4.2);
f(8, 'e', false); // Equivale a "f(8, 'e', false, 1.0)"
f(9, 'h'); // Equivale a "f(9, 'h', true, 1.0)"
f(10); // Equivale a "f(10, 'a', true, 1.0)"
f();  // Equivale a "f(3, 'a', true, 1.0)"

Si noti che usando valori di default non si hanno né vantaggi né penalizzazioni prestazionali; è semplicemente un'abbreviazione sintattica.

La seguente dichiarazione è illegale:

void f(int a = 3, double b);

Infatti, non è possibile dichiarare un valore di default per un parametro quando per uno o più parametri seguenti non è stato specificato un valore di default.

Inoltre, si consideri la seguente dichiarazione e le successive chiamate a tale funzione

void f(char *s = "abc", bool b = true);
void g() {
   f("abc", false); // Valido
   f(false); // Illegale
}

Nella prima chiamata a "f" si specificano entrambi i parametri. Nella seconda chiamata, il programmatore, ritenendo già soddisfacente il valore di default per il primo parametro, si è limitato a specificare il secondo parametro. Tale sintassi è illegale, in quanto i parametri si possono omettere solo progressivamente procedendo da destra a sinistra.

Il senso di ciò è che i parametri aventi valori di default sono stati pensati come parametri facoltativi in aggiunta a quelli obbligatori.

Anche le funzioni membro di una classe o struttura possono avere valori di default per i parametri.

Per esempio, la funzione "substr", della classe standard "string", può essere usata come nel seguente programma:

#include <iostream>
#include <string>
using namespace std;
int main() {
   string s = "abcdefgh";
   cout << s.substr(3, 2) << ","
      << s.substr(3) << ","
      << s.substr() << ","
      << s.substr(0, 3) << ".";
}

Questo programma stampa sulla console "de,defgh,abcdefgh,abc.".

La prima chiamata a "substr" specifica sia il punto da cui iniziare la sottostringa da estrarre che la sua lunghezza.

La seconda chiamata specifica solo il punto da cui iniziare la sottostringa, sottintendendo che la lunghezza è infinita. Se la lunghezza specificata per la sottostringa è eccessiva, viene estratta la parte rimanente della stringa.

La terza chiamata non specifica neanche il punto da cui iniziare la sottostringa, sottintendendo l'inizio della stringa originale. Ne risulta che tale chiamata effettua una copia dell'intera stringa.

La quarta chiamata è l'unico modo di estrarre una sottostringa contenente solo i primi tre caratteri, dato che non è possibile specificare la lunghezza della stringa senza specificare il punto iniziale.

Si consideri il seguente codice:

void f(int a = 0);
void f(int a = 0); // Illegale
void f(int a = 0) { } // Illegale

La prima e la seconda riga dichiarano la funzione "f", con quelli che in C si chiamano prototipi di funzione, mentre la terza riga definisce la stessa funzione. Questo codice è illegale, in quanto se una funzione viene prima dichiarata e poi definita, nelle dichiarazioni successive alla prima e nella definizione non sono ammessi parametri di default. Valgono quelli eventualmente specificati nella dichiarazione. La versione corretta è la seguente:

void f(int a = 0);
void f(int a);
void f(int a) { }

Ecco un esempio di programma completo valido:

#include <iostream>
#include <string>
using namespace std;
int f(int n = 1);
int f(int n);
int f(int n);
int f(int n) { return n * 3; }
int main() {
   cout << f() << " " << f(2);
}

Questo programma stampa "3 6".

I parametri di default nelle istanziazioni di templateModifica

Anche per i parametri dei template di classe sono ammessi valori di default, ma non per i template di funzione. Supponiamo di avere la definizione del seguente template di classe:

template <typename T1, typename T2 = T1> class C { };

È possibile istanziare tale template con le seguenti istruzioni:

C<int, double> a1;
C<int> a2;

La prima istruzione definisce la variabile "a1" di tipo "C<int, double>". La seconda istruzione definisce la variabile "a2" di tipo "C<int, int>".

Ecco un altro esempio, in un programma completo:

#include <iostream>
using namespace std;
template <int N1, int N2 = 0>
struct C {
   static int f() { return N1 + N2; }
};
int main() {
   cout << C<3, 5>::f() << " " << C<6>::f();
}

Questo programma stampa "8 6". Nella prima istanziazione del template di classe "C", il parametro di template "N1" riceve il valore 3 e il parametro "N2" riceve il valore 5, come specificato tra parentesi angolari. La funzione "f" si limita a rendere la somma tra questi due numeri. Nella seconda istanziazione del template di classe "C", il parametro di template "N1" riceve il valore 6, come specificato tra parentesi angolari, mentre il parametro "N2" riceve il valore di default 0.