Il MEG per sistemi quadrati | |||
Per testare la nostra classe fraction ho scritto una semplice versione
di un famoso algoritmo matematico per risolvere generici sistemi di equazioni
algebriche molto semplici, detti "lineari", in modo abbastanza efficiente,
dovuto al fisico-matematico tedesco Karl Friedrich Gauss (1777-1855), vissuto
molto prima dell'invenzione del primo vero calcolatore elettronico (l'ENIAC
a tubi elettronici del 1946).
Essendo questo un articolo sul C++ non spiegherò questo algoritmo, che troverete spiegato a parole in qualsiasi libro di algebra lineare. Vi dò invece quello che non troverete sui libri di algebra (a meno che non sia un libro davvero "smart" :) : il programma C++ corrispondente. #include "frac.h" int main()
cout << "* solve a squared
non-singular linear system\n"
// uses a GNU extension
cout << "input system matrix
by rows\n";
// 1. triangolarize
// swaps row step and k to place the pivot
// "triangolarize" this column
for (i=0; i<=n; ++i) // we
are reusing i
break;
// 2. back substitution
// 3. print solutions
cout << "\nbye :)\n"; exit(0);
Questo programma è quasi C, se si eccettua l'uso di cout e del tipo classe frac e il catch della eccezione, ma naturalmente deve essere compilato con un compilatore C++ con estensione opportuna (.cc, .C, .cpp, .cxx sono le estensioni più diffuse). L'algoritmo più generale permette di studiare e risolvere anche sistemi non quadrati, ma io per semplicità mi sono limitato ai soli sistemi quadrati e non singolari. Infatti se il sistema quadrato è singolare, può avere nessuna o infinite soluzioni. In quest'ultimo caso per descrivere le soluzioni occorre ricorrere al calcolo simbolico. Implementare una classe per fare calcolo simbolico è molto complicato. Chissà se avrò il tempo di farlo e di fare un tutorial. Il nostro programma per semplicità nel caso di sistema singolare stampa un messaggio di errore senza proseguire. Notare che il sistema di ordine n è rappresentato dalla matrice bidimensionale lsys n*(n+1), perchè questa è la soluzione più semplice. L'ultima colonna è la colonna dei termini noti. Questo spiega perchè l'indice di riga i viene spesso fatto scorrere fino ad n compreso, mentre l'indice di colonna k fino ad n escluso. Avrete notato che questa matrice è allocata sullo stack con una dimensione nota solo a runtime. Questo non è permesso dalla definizione del linguaggio e si tratta di un artefatto, in quanto non c'è alcun motivo per cui una istruzione del genere non dovrebbe essere possibile. Il compilatore della GNU la permette come estensione ed io credo risulti molto utile e la uso. Potrebbe non funzionare con il vostro compilatore: in questo caso dovrete prefissare una dimensione NMAX e sottoutilizzare la matrice. Non potete creare una matrice bidimensionale dinamica di dimensione giusto quanto basta, ossia variabile a run-time, perchè new frac[n][n+1] restituisce un puntatore ad un vettore di n+1 elementi di tipo frac (ricordate che i vettori bidimensionali in C/C++ non sono altro che vettori di vettori riga) e non potete scrivere cose del tipo: frac (*lsys)[n+1] = new frac[n][n+1]; in quanto il tipo di lsys non sarebbe noto a compile time, e il compilatore (e il linguaggio) non lo supportano. Se provate a compilare il programma di sopra, otterrete la seguente sfilza di errori: gauss.cc: In function `int main()':
Eppure il programma è corretto, è la nostra classe invece che presenta dei problemi. Il problema è semplice da capire: mancano gli operatori +=, -=, *= e /=. Se ne potrebbe fare a meno usando separatamente +,-,*,/ con =, (costringendo l'utente a scrivere f=f-g invece di f-=g) ma è meglio fornirli, per convenienza dell'utente e anche per ragioni di ottimizzazione. Essi sono delle funzioni membro, in quanto l'operando di sinistra è sempre di tipo fraction. Chiamate ad es. del tipo double & /= fraction & sono gestite dall' operator /=(double,double) <builtin> con la collaborazione del nostro convertitore di tipo (che converte fraction in un double), quindi non vogliamo gestirle noi e perciò non usiamo il meccanismo degli operatori friend. In altri termini in un caso del genere la frazione viene prima convertita ad un double e poi avviene una operazione di /= tra double ad opera della "classe double" e non della nostra classe (i tipi predefiniti possono essere infatti visti concettualmente come una specie di classi "native", con qualche restrizione, perchè in realtà non sono tipi classi, non sono implementati con sorgenti, ma il compilatore ne ha una conoscenza diretta; una restrizione ad es. è che non potete derivare una classe da un tipo nativo con il meccanismo dell'ereditarietà). Notare che non abbiamo bisogno di scrivere le nostre versioni di operator=, del costruttore di copia e del distruttore. Vanno bene quelle fornite dal compilatore. Perchè il compilatore, anche se sa come assegnare una frazione ad un'altra (assegna semplicemente i membri dati _num e _denom rispettivi), non sa come effettuare /=? Perchè non sa come dividere le due frazioni se potrebbe chiamare operator/ che l'abbiamo definito? Perchè non lo fa? Semplicemente perchè il linguaggio non stabilisce la definizione implicita di questi operatori come fa con il costruttore senza argomenti, l'operatore di assegnamento, il costruttore di copia e il distruttore. Una definizione implicita da parte del compilatore comunque non sarebbe possibile se non fosse definito l'operator/ tra oggetti della classe. Nessun problema grave comunque, banalmente possiamo dire: const fraction& operator /=(const fraction& divider)
const fraction& operator -=(const fraction& subtrahend)
Queste definizioni di macro fanno fare al compilatore quello che non voleva fare: utilizzare = e /,- per implementare /= e -= rispettivamente. Fatto questo ricompilate e provate a risolvere un sistema non singolare. Ad es. questo piccolissimo sistema 2x2 ha per incognite i due numeri (8 e 3) che sommati danno 11 e sottratti danno 5: x+y=11
Non me ne vogliano i matematici per aver scelto un esempio così banale. Il programma è in grado di risolvere sistemi di qualsiasi dimensione (avendo tempo e memoria a disposizione). L'input va inserito in questo modo: 1 1 11
o se preferite scrivete ogni numero su una linea a se stante: 1
Ecco poi un sistemino più complicato tratto da un libro di matematica: 2x1 + x2 -5x3 + x4 =8
Ed ecco la soluzione esatta:
Uhm, qui non si vede nessuna vera frazione. Provate questo: 2x + y + 5z + t = 5
input:
output:
#2:
#3:
#4:
Per velocizzare un po' gli operatori /=(const fraction&) e simili, possiamo evitare la chiamata dell'operatore di assegnamento e del costruttore (necessaria per ritornare il valore della divisione). Ecco la nuove versioni ottimizzate, che modificano direttamente lo stato dell'oggetto proprio: const fraction& fraction::operator /=(const fraction& divider)
const fraction& fraction::operator -=(const fraction& subtrahend)
Lo svantaggio è che il codice della classe (sia sorgente che
oggetto) sarà più lungo.
Possiamo anche velocizzare gli operatori /=(long)e simili, evitando la chiamata del costruttore per convertire il long in una frazione temporanea (che poi dovrà essere distrutta) e dell'operatore di assegnamento e risparmiando qualche operazione aritmetica. Ecco come definirli esplicitamente: const fraction& fraction::operator /=(long divider)
const fraction& fraction::operator -=(long subtrahend)
L'idea è semplice: utilizziamo le seguenti formule semplificate: a a
1 a
e a a
n*b a-n*b
Analogamente implementiamo le versioni ottimizzate di *=(long) e +=(long). Un dubbio mi assale: occorre chiamare reduce( ) in +=(long) e -=(long) o no? Ecco perchè ho posto quel ? nel codice di prima. Se non occorre chiamarlo e possiamo risparmiarcelo ottimizziamo un pochino. Solo la matematica può rispondere a certe domande. Ecco quindi ancora una volta che la matematica entra prepotentemente nella pratica della programmazione. nota: tutti i numeri nominati in questo discorso sono intesi essere interi. Teorema: supponiamo che a/b sia ridotta ai minimi termini, cioè non esiste un numero k tale che: a=k*q
allora a-n*b, il numeratore di a/b-n, non ha fattori comuni con b, ovvero la frazione differenza: a-n*b
è già ridotta ai minimi termini (perciò non occorre chiamare reduce( )). Dimostrazione: dobbiamo dimostrare che a-n*b non ha fattori comuni con b, cioè che nelle ipotesi di prima non può esistere un t tale che: a-n*b=t*x
supponiamo che esista. Allora si avrebbe: a=t*x+n*b
ossia sostituendo la seconda nella prima: a=t*x+n*t*y
e mettendo in evidenza t nella prima: a=t*(x+n*y)
ponendo q=(x+n*y) e s=y si vede che abbiamo contraddetto le ipotesi
con la supposizione che t esista. Allora per logica si conclude che t nelle
ipotesi del teorema non può esistere e questo dimostra il teorema
(dimostrazione per assurdo). Questo teorema era del tipo che afferma la
non esistenza di qualcosa sotto certe ipotesi. Di solito teoremi di questo
tipo si risolvono facilmente con la regola logica della dimostrazione indiretta
(che è un assioma), che è quella che abbiamo appena usato
(è detta anche dimostrazione per assurdo):
A nel nostro caso era che "t che è un fattore comune ad a-n*b e b esiste" e ciò aveva come conseguenza non B="k è un fattore comune ad a e b" come abbiamo dimostrato. Ma poichè per ipotesi vale B="a e b non hanno fattori in comune", allora A, che ricordo è "t che è un fattore comune ad a-n*b e b esiste" non può essere vera, ossia è vero il contrario: "non esiste un fattore comune a a-n*b e b" questa è proprio la tesi del teorema. Per una introduzione alla logica matematica e altro vi consiglio di leggere il libretto "MATEMATICA SENZA NUMERI" di Giuliano Spirito, edito da Newton. E' un tascabile economico e costa poco più di mille lire. Questo teorema ammette il suo duale sostituendo ovunque in segno - col +. C'è ancora del lavoro da fare. Notate la linea 30 di gauss.cc: if (lsys[k][step]!=0) perchè occorre indicare esplicitamente quel !=0? Provate ad ometterlo, vedrete che tutto funziona, ricompilando e riprovando a risolvere i sistemi di prima. Cosa accade? Potete verificare che non viene chiamato infatti l'operator!=(const fraction&,long), ma viene chiamato il convertitore a double. Questo va altrettanto bene. Per la cronaca, per far sì che venga chiamato operator!=(const fraction&,long) o per gestire in qualsiasi modo la situazione, basta definire un altro convertitore di tipo: operator bool( ) const { return _num!=0; } Questo evita la divisione che effettua double, inoltre lo ritengo più appropriato a trattare la situazione perciò l'ho inserito. Altri miglioramenti: la classe permette di convertire una fraction in double. Che ne dite di dare la possibilità di convertire un double in una fraction? Sto per aprire un'altra scatoletta di vermi. Infatti una operazione di questo genere porta alla ribalta il problema dell'overflow, che finora abbiamo ignorato. Ho infatti ignorato il problema dell'overflow, anche se avrei potuto prendere dei provvedimenti per segnalarlo, o addirittura evitarlo in qualche caso. La ragione per cui non l'ho fatto non è solo per non introdurre troppa complessità, ma anche perchè personalmente ho voluto progettare fraction in modo che sia in grado di operare efficientemente su numeratori e denominatori di non prefissata grandezza, usando un'altra classe che implementa interi a precisione illimitata (che devo ancora scrivere). In questo caso generalmente non mi serve controllare condizioni di overflow. Nel caso del costruttore a partire da double però si pone un problema: i double hanno solitamente molte cifre decimali di precisione (almeno 10 e tipicamente 15 come sul mio pc - DBL_DIG è una costante standard definita in <float.h> che indica il numero di cifre decimali di precisione). Un long invece, stabilisce lo standard, è minimo di dimensione 32 bit (tale è sui PC al momento in cui scrivo). Ora un long signed di 32 bit può assumere valori tra: [-231 ; 231-1] oppure [-2,147,483,648 ; 2,147,483,647] perciò possiamo dire che possiamo rappresentare valori decimali
di massimo 9 cifre (da -999,999,999 a +999,999,999).
Idealmente, se avessimo un numero irrazionale, il procedimento non avrebbe mai termine. I double comunque hanno un numero limitato di cifre e sono un sottoinsieme non solo dei numeri reali, ma persino di quelli razionali, non essendo i double un "continuo" di valori, ma essendo in numero finito. L'algoritmo precedente tuttavia funzionerebbe male a causa dell'overflow. Ad es. se abbiamo 10 miliardi essi non entrano in un long e non possiamo convertirli in frazione con long in nessun modo. Tuttavia ignoreremo questo caso, come spesso abbiamo fatto. Non possiamo però ignorare il caso in cui il numero double sarebbe rappresentabile come frazione, a costo di ridurre la precisione. Ad es. se abbiamo 15 cifre di precisione per un double, questo può essere memorizzato in modo esatto: 1.23456789012345 ma la sua conversione secondo l'algoritmo precedente genera un errore di overflow, quando si va a moltiplicare per 10 il numero 1,234,567,890, perchè il risultato (più di 10 miliardi) non è rappresentabile con un long. E' evidente che dobbiamo fermarci ed esprimere quindi il numero in forma di frazione approssimata, così: 1,234,567,890
Quindi modifichiamo l'algoritmo precedente in questo modo, essendo LONG_DIG il numero massimo di cifre di cui può essere composto un qualsiasi long nel nostro caso (long a 32 bit) LONG_DIG è 9: Per convertire un double in una frazione, useremo il seguente algoritmo pratico: lo moltiplicheremo per una potenza di 10 finchè non risulta un numero intero e finchè abbiamo ottenuto un numero di cifre della parte intera minore di LONG_DIG; mettendo poi a numeratore il risultato del precedente ciclo e a denominatore la potenza di 10 (che sarà rappresentabile) e riducendo ai minimi termini, avremo la frazione corrispondente. Nel caso precedente, rappresentiamo dapprima 1.23456789012345 con questa frazione: 123,456,789
e poi la riduciamo ai minimi termini (ma sfortunatamente rimane così). Usiamo fino 9 cifre perchè come abbiamo detto in generale con un long possiamo memorizzare numeri decimali qualsiasi di massimo 9 cifre. Ovviamente nel caso in cui si hanno poche cifre di precisione, come quando si converte 0.5 in una frazione, il nostro algoritmo genera 5/10 che ridotto fa 1/2 e la conversione è esatta. Ovviamente operando con frazioni con grossi numeratori e/o denominatori, è facile incorrere nel problema dell'overflow, che abbiamo ignorato nell'implementazione degli operatori. Almeno però questo operatore di conversione si comporterà in modo abbastanza corretto. Ripeto che una volta che cambiamo long con un tipo intero esteso, non ci sono più problemi nella conversione double->fraction, in quanto presumibilmente il tipo esteso sarà in grado di contenere tutte le cifre di precisione di un double e anche oltre. Si potrebbe comunque volere convertire il double in fraction con una precisione che arriva fino ad un certo numero di cifre. Ecco quindi che ho creato un parametro p con valore di default FTYPE_DIG (d'ora in poi ho deciso di stabilire che il tipo dei componenti di una frazione è fitype - fraction integer type): se non specificate nulla, viene utilizzato il massimo numero di cifre di precisione per il tipo intero con cui vengono rappresentate le frazioni, altrimenti viene utilizzato il numero di cifre di precisione rappresentato, con la limitazione che si cerca sempre di utilizzare tutte le cifre della parte intera, anche se maggiori del p specificato e si può continuare ad incorrere in overflow (come nel caso in cui d sia 10 miliardi). In pratica il conto del p specificato include quello della parte intera. Se gli interi sono a precisione illimitata, FITYPE_DIG è un valore di default preferenziale, altrimenti (come nel caso dei long) non dovreste mai specificare un p maggiore di esso. fraction(double d, char p=FITYPE_DIG); // dichiarazione fraction::fraction(double d, char p=FITYPE_DIG) // definizione
if ((fitype)d==0)
while (d!=(fitype)d && e<p)
Es.
double d=0.34;
Il problema con la definizione di questo costruttore-convertitore è che adesso una istruzione tipo: fraction f(1,3); non viene compilata, perchè si ha una solita ambiguità nella chiamata delle funzioni sovraccariche: call of overloaded `fraction(int, int)' is ambiguous
vogliamo obbligare l'utente a scrivere fraction f((fitype)1,(fitype)3);
o f((fitype)1,3); ?
void fraction::setfd(double d, char p=FITYPE_DIG); // definizione fraction::setfd(double d, char p=FITYPE_DIG) // definizione
Quindi gli esempi di prima vanno riscritti così (la sintassi è peggiorata, per mantenere una sintassi semplice dell'inizializzazione con frazione, ma non posso farci niente): double pi=3.14159;
double d=0.34;
Non si può avere tutto nella vita. Analizziamo ora cosa accede mischiando double e frazioni. Il programmino di test è questo: #include "frac.h" int main()
f=f+2.3; f.display(); exit(0);
Questo programmino si compila senza problemi, ma l'output potrebbe stupire: 3. Questo perchè 2.3 (double) viene convertito ad intero e viene chiamato operator+(const fraction& f, fitype n). Anche qui però non possiamo fare molto. Possiamo definire un altro operator+(const fraction& f, double d) e il suo duale operator+(double d, const fraction& f), ma avremmo comunque ambiguità quando scriviamo ad es. f=f+2. La soluzione migliore, visto che le conversioni di double in fraction sono presumibilmente poco usate (se avete un tipo fraction senza limitazioni, come sarà quando userà interi a precisione illimitata, perchè usare double, se non per convertire il risultato finale?) è di rendere la sintassi "cluttered" e le operazioni miste meno efficienti in questo caso, anzichè nel caso di conversioni da int a fraction, che supponiamo più frequenti. Quindi non definiamo niente di nuovo e allora il prg. di prima va scritto così: #include "frac.h" int main()
tmp.setfd(2.3);
f.display(); exit(0);
Possiamo pulire un po' la sintassi definendo una funzione friend della classe fraction (che non viene cioè invocata su un oggetto) e che restituisce una fraction corrispondente al double passato come argomento. In pratica effettua su richiesta la conversione double->fraction. Eccola: fraction ffd(double d, char p=FITYPE_DIG)
Potete anche fare il contrario: cioè setfd chiama ffd.Questa può essere anche una funzione globale, ma l'ho definita come friend per sottolineare che è in qualche modo legata alla classe. Avrei potuto usare la dichiarazione static per ffd, in questo modo: static fraction ffd(double d, char p=FITYPE_DIG); // dichiarazione pubblica fraction fraction::ffd(double d, char p=FITYPE_DIG) // implementazione
ma questa obbliga a riferirsi alla funzione come fraction::ffd, mentre io voglio una sintassi più semplice: basta usare solo ffd L'ho chiamata con lo strano nome ffd perchè è un nome breve. Sta per Fraction From Double. Il programma allora si scrive così: #include "frac.h" int main()
f=f+ffd(2.3); // f=33/10 f.display(); exit(0);
la definizione e dichiarazione di ffd vanno fatte come friend in questo modo: // type converter on demand
fraction ffd(double d, char p=FITYPE_DIG)
Una soluzione di compromesso quindi, che rende la sintassi non ambigua e più semplice per le funzionalità più frequentemente utilizzate. Finiamo qui. Ricordo che il codice finale della classe si trova nello zip. Usalo liberamente nei tuoi programmi; se fai qualche miglioramento alla classe sarebbe gradito spedirmelo. Ciao. Ecco alcuni: Esercizi: 1) modificare la funzione membro pow in modo che potenze del
tipo
11,000,000,000, -11,000,000,000, -11,000,000,000, -1-1,000,000,000, 01,000,000,000 e simili siano calcolate immediatamente. 2) cosa accade con questo codice? double d=4;
d+=f;
come risolvereste il problema, se possibile? Soluzione: ambiguous overload for `double & += fraction &'
la "colpa" è del convertitore di tipo fraction->bool. Una possibile soluzione sarebbe quella di non definire questo convertitore, che come osservato è nella maggior parte dei casi opzionale; un'altra sarebbe quella di costringere l'utente ad usare un cast esplicito: d+=double(f);. Preferisco questa, essendo poco frequente la necessità di convertire frazioni a double, meglio farlo in modo esplicito. 3) Quali sono gli svantaggi e vantaggi di questa definizione più semplice dell'operator+ (o di una analoga per operator-)? fraction operator+(const fraction&) const; // dichiarazione fraction fraction::operator+(const fraction& f) // definizione
Soluzione: Questa dichiarazione è ovviamente più semplice, perchè non calcola l'mcm e quindi non dobbiamo scrivere la nuova funzione mcm. Ci basiamo infatti sulla seguente semplice formula (peraltro in generale sconsigliabile quando si opera manualmente) e sulla funzione reduce già scritta, chiamata automaticamente dal costruttore con argomenti: a c
a*d b*c a*d+b*c
il trucco usato per ridurre le due frazioni allo stesso denominatore (notate che questo in generale non è più il minimo, per questo occorre poi chiamare reduce( ) per ridurre il risultato ai minimi termini) è quello di moltiplicare numeratore e denominatore della prima per il denominatore della seconda e viceversa. Lo svantaggio di questo codice se le frazioni sono rappresentate tramite long è che è maggiormente soggetto al problema dell'overflow. Andrebbe invece abbastanza bene con interi lunghi. Notate che anche con gli operator +=(fraction) e -=(fraction) potremmo agire allo stesso modo, quindi non avremmo più bisogno di mcm. Notate che ho cambiato la definizione come funzione membro anzichè friend. Questo ha lo svantaggio di non poter fare più una somma del tipo ffd(d)+f dove f è una frazione e d un double. prototype for
Questo è un altro svantaggio introdotto appositamente. Se l'avevate individuato complimenti, altrimenti non disperate. Ci sono alcuni (molti?) professori universitari che insegnano il C++ e non sanno programmare e sicuramente sono peggio di voi. 4) Definire un operator ! (not logico) per la classe fraction e spiegarne l'utilizzo. Soluzione: un operator ! (not logico) applicato ad una fraction si dovrebbe comportare in modo analogo a come si comporta ad es. col tipo nativo int. Se il numero è 0, il risultato (un booleano) è true, altrimenti è false. La sua definizione è semplicissima (il corpo è praticamente il negato di quello del convertitore fraction->bool): bool operator!( ) const { return _num==0; } Un esempio di uso: fraction f; if (!f)
Personalmente preferirei scrivere esplicitamente f==0 perchè è più leggibile e forse altrettanto efficiente. 5) (collegato al precedente) Per poter usare operator! nell'ultima versione della nostra classe, è necessario definirlo esplicitamente? Soluzione: No, perchè esiste il convertitore a bool. Se anche non ci fosse verrebbe chiamato il convertitore a double e poi l'operator !(bool) <builtin>. Se nemmeno questo convertitore ci fosse, allora sarebbe generato un errore. Tuttavia definire esplicitamente bool come fatto prima è in generale più efficiente, perciò ho aggiunto la definizione nella sezione intitolata // unary operators // 6) Cosa c'è che non va con questa definizione dell'operator< e simili? friend bool operator<(const fraction& f1, const fraction&
f2)
oppure friend bool operator<(const fraction& f1, const fraction&
f2)
Soluzione: queste funzionano abbastanza bene con i long, ma nel caso di interi estesi, occorre avere un convertitore a double che comunque può comportare una perdita di efficienza e produrre confronti sbagliati. Perciò non ho usato queste versioni. 7) Aggiungere gli operatori ++ e -- sia prefissi che postfissi alla classe fraction. Fate in modo che siano efficienti, come avviene sui tipi nativi, anche con le ottimizzazioni del compilatore disabilitate. Soluzione: ecco le versioni generalmente inefficienti di questi operatori unari, che si limitano a chiamare l'operatore + e quello di assegnamento o meglio ancora l'operator +=: inline fraction operator++( ) { return (*this)+=1; }
inline fraction operator--( ) { return (*this)-=1; }
Essi restituiscono un rvalue come con i tipi nativi. Il parametro fittizio distingue la versione postfissa dell'operatore da quella prefissa. Notate come nel caso postfisso il valore da ritornare deve essere quello prima dell'incremento; un modo per ottenere ciò è salvare in una temporanea il vecchio valore prima di decrementarlo e restituire poi la temporanea per valore. Ecco le versioni ottimizzate, ottenute fondendo il codice di prima con quello di operator +=(fitype) o -=(fitype) ponendo l'argomento pari ad 1: Notare che non occorre ridurre ai minimi termini. Si può dimostrare ad es. che se a/b è irriducibile, allora anche a/b+1=(a+b)/b lo è necessariamente. // prefissi
// postfissi
fraction fraction::operator--(int)
8) Qual'è la differenza tra queste due definizioni dell'operator+ (e operatori o funzioni che similmente restituiscono un valore non const e un valore const)? friend fraction operator+(const fraction&, const fraction&);
Soluzione: nel primo caso è permesso alterare il valore restituito. E' possibile scrivere cose del tipo (a e b sono frazioni): cout << ++(a+b);
Questi usi sono entrambi vietati con i tipi nativi. Il primo avrebbe un senso, ma è più corretto scrivere: cout << a+b+1; Il secondo uso non ha senso di essere usato. E' equivalente a scrivere: cout << c; Perciò ho deciso di aggiungere il qualificatore const per tutti i valori restituiti: meglio inibire usi strani e inutili. Per i tipi classe occorre esplicitamente indicare const per restituire un r-value, altrimenti la temporanea restituita potrà essere usata anche come lvalue. Notare che questo non inibisce espressioni tipo -(a+b); in questo caso il valore restituito dall'operator+(fraction,fraction) viene usato come oggetto proprio di operator-; siccome questo è definito come metodo const, non ci sono problemi; il - restituisce un altro valore, non altera l'oggetto proprio. Analogamente dovremmo tramutare fitype num() const { return _num; }
in const fitype num() const { return _num; }
in modo che non siano possibili strani usi come ++f.num( ) (che in realtà non altera il numeratore di f come farebbe credere, ma solo una copia temporanea), quando fitype è un tipo classe (come una classe di interi a grande precisione). In realtà in questo caso conviene restituire un riferimento invece che un valore, perchè il tipo fitype potrebbe essere grosso (se ad. implementato tramite un array) e vogliamo evitare l'overhead di copia. In questo caso dobbiamo ritornare un riferimento a costante se non vogliamo permettere usi del tipo ++f.num( ) o f.num( )+=10, e non un riferimento a non-costante. Questo anche perchè in tal caso potremmo modificare il numeratore, senza poi che la frazione sia ridotta ai minimi termini e anche perchè non potremmo più definire come const il metodo ed applicarlo ad un oggetto frazione costante (cosa che dovrebbe essere possibile: dovrebbe essere possibile ad es. interrogare la frazione costante chiamata un_mezzo per sapere che il numeratore è 1). Questo programmino illustra quest'ultimo sottile problema: class test
//const test ct;
return(0);
test.cc: In method `int & test::access()
const':
la conversione da `const int' a `int &' infatti permetterebbe la modifica di un oggetto costante, perciò il programma non viene compilato. Incidentalmente notate che questo si compila, ma è buona norma non fare uso di questa tecnica e definire una funzione accessore di setting. class test
test ct;
return(0);
Perciò ho preferito usare un riferimento costante: const fitype& num() const { return _num; }
Se volete alterare il valore del numeratore di una frazione non costante, per es. incrementandolo di 1, potete scrivere: f.set_num(f.num( )+1); che è anche più chiaro da leggere, anche se un po' più prolisso. Notare che nel caso degli operatori ++ e -- postifissi scritti nell'esercizio precedente, non possiamo ritornare un riferimento all'oggetto temporaneo locale tmp, perchè sarebbe un riferimento fluttuante, cioè non valido, in quanto questo non esiste più all'uscita della funzione. Lo stesso per operator^ e ffd. Se vogliamo impedire di modificare l'oggetto ritornato per valore o riferimento, scriveremo: const fraction& operator++( ) { _num+=_denom; return (*this);
} // prefix
Come in C quindi i nostri operatori ++ e -- sia postfissi che prefissi, restituiscono degli rvalue, ossia valori non modificabili. Non sono permesse cose del tipo ++(++n), che non servono in quanto è meglio scrivere n+=2. Anche ffd e operator^ restituiscono ora un valore costante. |
![]() |
![]() |
![]() |