Salta al contenuto
Note per Studenti C++ - classi, membri e invarianti

C++ - classi, membri e invarianti

In questa pagina 7

Come progettare un tipo nuovo, seguendo l'esempio della classe Date (data del calendario) attraverso versioni successive: ogni iterazione la rende più utile e più sicura, come nello sviluppo vero del software, dove a ogni passo si prova il codice. Le classi sono la base di tutto il resto: costruttori e copia sono in C++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_list permette la lista {a, b, c} e con le graffe ha la precedenza sul costruttore a un intero (vector{3} è un elemento, vector(3) sono tre); la copia di default è membro a membro (shallow copy) e con un puntatore a un buffer duplica il puntatore invece del contenuto (due oggetti che condividono i dati e una doppia delete), quindi servono il costruttore di copia e l'assegnamento di copia che fanno la deep copy (l'assegnamento prima alloca e copia, poi libera i vecchi dati, e deve tollerare l'auto-assegnamento); this è il puntatore all'oggetto corrente, parametro implicito delle funzioni membro; il costruttore e l'assegnamento di spostamento (T&&, rvalue reference) rubano il buffer della sorgente lasciandola vuota ma valida, il compilatore li sceglie da solo (con la copy elision a volte non servono nemmeno); i cinque (distruttore, copia, spostamento) vanno definiti insieme.C++ - costruttori, inizializzazione, copia e spostamento →, gli operatori in C++ - enumerazioni e overloading degli operatoriun'enumerazione è un tipo definito dall'utente che specifica un insieme di valori simbolici (enumeratori); enum class crea uno scope (Month::jan), non si converte implicitamente in int, e i valori partono da 0 o dal valore del precedente più uno; l'overloading degli operatori permette di definire gli operatori esistenti per tipi definiti dall'utente (almeno un operando UDT, stesso numero di operandi e stessa sintassi) scrivendo una funzione di nome operator+, operator++, operator[], ...; si può fare con una helper function (due argomenti per un operatore binario, il primo può essere di qualunque tipo) o con una funzione membro (un argomento in meno, perché il primo operando è l'oggetto, quindi l'UDT deve stare a sinistra: T + int sì, int + T no); << e >> sono helper function che prendono e restituiscono ostream& e istream&; ++ ha due forme (pre e post, quest'ultima con un int fittizio); gli operatori più utili sono =, ==, !=, <, [], ().C++ - enumerazioni e overloading degli operatori →, l'ereditarietà in C++ - ereditarietà, funzioni virtuali e layout degli oggettiL'ereditarietà pubblica (class D : public B) esprime la relazione "is a": D eredita da B i membri public e protected e li estende; la composizione esprime invece "has a". Una classe base di una gerarchia disabilita copia e assegnamento (=delete) perché copiando una derivata in una base si ha lo slicing. Una funzione virtuale può essere ridefinita nelle derivate (override: stesso nome, tipi e constness) e la funzione chiamata si decide a run-time in base al tipo dell'oggetto, non del puntatore o del riferimento; override esplicito con la keyword override; una funzione virtuale pura (= 0) obbliga le derivate a ridefinirla e rende astratta la classe; il distruttore di una classe base polimorfa è virtuale. Il meccanismo è realizzato con una vtbl per classe (tabella di puntatori alle funzioni) e un vptr in ogni oggetto; i dati della derivata seguono quelli della base.C++ - ereditarietà, funzioni virtuali e layout degli oggetti →. Chi conosce le struct del C (Strutture (struct) in Cstruct per raggruppare campi di tipo diverso, typedef, accesso con punto e freccia, struct come parametri e valori di ritorno, array di struct, struct allocate dinamicamente e struct autoreferenziali.Strutture (struct) in C →) o le classi di Python (Classi ed ereditarietà in PythonAttributi di istanza e di classe, metodi di istanza, di classe e statici; confronto e ordinamento con eq e lt; proprietà; ereditarietà, super(), override e polimorfismo; classi astratte come interfacce; eccezioni personalizzate; overloading e shadowing in Python.Classi ed ereditarietà in Python →) ritrova l'idea; l'astrazione sui dati è in Tipi di dato astrattiUn ADT è definito dalle operazioni e dal loro comportamento, non dalla rappresentazione; interfaccia e implementazione; realizzazione in Python con le classi e in C con header e tipo opaco; esempio di un ADT Frazione.Tipi di dato astratti →.

Tipi definiti dall'utente

C++ ha un forte focus sui tipi. Quelli built-in sono int, char, double, ...; i tipi definiti dall'utente (UDT, user defined types) sono le classi della libreria standard (string, vector, ostream) e quelle scritte dal programmatore. vector, string, list sono costruiti con le stesse tecniche delle classi del programmatore: il C++ è fatto in modo che chi scrive la libreria standard non abbia "superpoteri".

"Types are good for directly representing ideas in code". Un tipo nuovo ha due elementi chiave:

elemento significato
rappresentazione come il tipo rappresenta i dati dentro un oggetto: il valore corrente o stato
operazioni che cosa si può fare con un oggetto, cioè con i dati che lo rappresentano

Anche una macchinetta del caffè o un motore hanno uno stato (livello di acqua, temperatura; giri, carburante o carica). Se si pensa a qualcosa come a un'entità separata, probabilmente si può mappare in una classe. Una classe specifica come sono rappresentati i suoi oggetti, come si creano, come si usano e come si distruggono. (Il termine "oggetto" è un'istanza di una classe.)

Classi, membri, interfaccia

Una classe è composta da tipi built-in, altri UDT e funzioni. Le entità usate per definirla sono i membri: dati membro (data member) e funzioni membro (function member).

cpp
class X
{
public:
    int m;
    int mf(int v) { int old = m; m = v; return old; }
};

X var;
var.m = 7;           // accesso a un dato membro: oggetto.membro
int x = var.mf(9);   // chiamata di una funzione membro; dentro la classe non serve oggetto.

Il principio guida è interfaccia contro implementazione:

parte punto di vista accesso
interfaccia (public) di chi usa la classe accessibile a tutti
implementazione (private) di chi scrive la classe non accessibile; lettura e scrittura solo tramite l'interfaccia

Questo permette di controllare lo stato (la coerenza) dei dati. Per default i membri di una class sono privati. Una struttura classica:

cpp
class X
{
public:
    // interfaccia utente: funzioni, tipi, (più raramente) dati
private:
    // dettagli implementativi: funzioni, tipi, dati
};

Definizione. Una struct è una variante della class con un'unica differenza tecnica: i membri sono pubblici di default. Si usa quando ci sono dati ma non funzioni (retaggio del C).

Gli strumenti che C++ dà per progettare una classe sono: funzioni membro, costruttori, protezione dei dati, overloading degli operatori, gestione della copia, helper function: li incontriamo in questo ordine.

La classe Date, versione per versione

Versione 1: una struct pubblica

cpp
struct Date { int y; int m; int d; };   // anno, mese, giorno
Date today;
today.y = 2005; today.m = 12; today.d = 24;

Si può fare tutto, anche l'assurdo: x.y = -3; x.m = 13; x.d = 32; compila ed esegue; y.y = 2000; y.m = 2; y.d = 29; è corretto (il 2000 è bisestile) ma il compilatore non lo sa e non lo controlla. L'uso è lento e poco sicuro. Per le operazioni più comuni si scrivono helper function, "programmate e debuggate una sola volta", come init_day(Date&, int y, int m, int d) (controlla e inizializza) e add_day(Date&, int n).

date_v1.cppcpp
#include <iostream>
using namespace std;

struct Date {            // versione 1: tutto pubblico
    int y;               // anno
    int m;               // mese
    int d;               // giorno
};

// helper function: operazioni comuni scritte e provate una volta sola
bool init_day(Date& dd, int y, int m, int d)
{
    if (m < 1 || m > 12 || d < 1 || d > 31) return false;   // controllo (semplificato)
    dd.y = y; dd.m = m; dd.d = d;
    return true;
}

int main()
{
    Date today;                                // non inizializzata: valori casuali!
    init_day(today, 2008, 3, 30);              // la helper function c'e'...
    cout << today.y << '-' << today.m << '-' << today.d << '\n';

    Date tomorrow;
    tomorrow.y = today.y;
    tomorrow.m = today.m;
    tomorrow.d = today.d + 1;                  // ...ma si puo' costruire a mano, e bene
    cout << tomorrow.y << '-' << tomorrow.m << '-' << tomorrow.d << '\n';

    Date x;                                    // niente impedisce date assurde
    x.y = -3; x.m = 13; x.d = 32;
    cout << x.y << '-' << x.m << '-' << x.d << '\n';
    return 0;
}
2008-3-30
2008-3-31
-3-13-32

Il problema: le helper function ci sono ma si possono bypassare. today può restare non inizializzato e tomorrow si costruisce a mano (tomorrow.d = today.d + 1 può dare 31 marzo, ma il 31 gennaio +1+1 sarebbe un 32 gennaio): non c'è modo di rendere obbligatorio l'uso delle funzioni. Serve rendere i dati inaccessibili e le funzioni obbligatorie.

Versione 2 e 3: funzioni membro, costruttore, dati privati

Le funzioni membro sono parte integrante della classe (scope di classe): Date(int y, int m, int d); e void add_day(int n);. I dati si rendono private; l'accesso diventa possibile solo attraverso le funzioni pubbliche: l'interfaccia.

date_v3.cppcpp
#include <iostream>
using namespace std;

class Date {                    // versione 3: dati privati, costruttore, accessi
public:
    Date(int yy, int mm, int dd);   // il costruttore e' l'UNICO modo di creare una Date
    void add_day(int n);
    int year() const { return y; }
    int month() const { return m; }
    int day() const { return d; }
private:
    int y, m, d;
};

Date::Date(int yy, int mm, int dd)  // lista di inizializzazione dopo i due punti
    : y{yy}, m{mm}, d{dd}
{
}

void Date::add_day(int n)
{
    d += n;                          // (versione semplificata: ignora la fine del mese)
}

int main()
{
    Date birthday {1970, 12, 30};    // ok: costruttore
    // Date my_birthday;             // errore: nessun costruttore senza argomenti
    // birthday.m = 14;              // errore: m e' privato
    birthday.add_day(1);
    cout << birthday.year() << '-' << birthday.month() << '-' << birthday.day() << '\n';
    return 0;
}
1970-12-31
  • Date birthday {1970, 12, 30}; invoca il costruttore. Date my_birthday; non compila: non c'è un costruttore senza argomenti, quindi non si può creare una Date non inizializzata (con solo un costruttore con argomenti, ometterli è un errore di compilazione).
  • birthday.m = 14; non compila: m è privato. Si legge con month(), day(), year().
  • Il costruttore usa una lista di inizializzazione (member initializer list) dopo i due punti: : y{yy}, m{mm}, d{dd}.

Il costruttore è una funzione membro con lo stesso nome della classe, usata per inizializzare (costruire) gli oggetti; il suo compito è dare ai dati uno stato di partenza valido: valori di default validi oppure, se riceve argomenti, un controllo di validità.

Lista di inizializzazione contro assegnamenti. L'alternativa è

cpp
Date::Date(int yy, int mm, int dd) { y = yy; m = mm; d = dd; }

ma richiede due passi (inizializzazione di default e poi assegnamento), mentre la lista inizializza direttamente con il valore giusto, evita il doppio passaggio ed esprime meglio l'intento. Ci sono tipi che devono essere inizializzati nella lista (membri const e reference).

Come si può chiamare un costruttore:

cpp
Date last {2000, 12, 31};          // ok, preferibile
Date next = {2014, 2, 14};         // ok, un po' verboso
Date christmas = Date{1976, 12, 24}; // ok, un po' verboso
Date last2(2000, 12, 31);          // ok: vecchio stile
Date my_birthday;                  // errore: non inizializzato
last.add_day(1);                   // ok
add_day(2);                        // errore: quale data?

Stato e invarianti

Le funzioni membro garantiscono che i dati siano sempre coerenti: suppongono che lo siano in ingresso e garantiscono che lo siano in uscita; cioè l'oggetto è sempre in uno stato valido. L'alternativa (controllare a ogni uso o sperare che l'utente non metta dati sbagliati) non funziona: "Experience shows that 'hoping' can lead to 'pretty good' programs".

Definizione (invariante). Un invariante è una regola che definisce i valori validi dello stato di un oggetto, e che il costruttore stabilisce e ogni funzione membro conserva.

Per Date gli invarianti non sono ovvi: mese tra 1 e 12, giorno tra 1 e il numero di giorni di quel mese, anni bisestili, calendario gregoriano, fusi orari. Per Rational (Esercizio - Numeri razionali con classe e overloading (laboratorio 2)): denominatore diverso da zero e frazione ridotta ai minimi termini. Una classe ben progettata non entra mai in uno stato non valido; il primo passo è saper rilevare l'errore.

Versione finale: rilevare gli errori

is_valid() è una funzione separata dal costruttore (e privata) perché è un'attività diversa dall'inizializzazione e può servire a tutti i costruttori. L'esito si segnala in uno di questi modi: codici di ritorno, scrittura su un membro, eccezione. La classe Invalid è un tipo vuoto, definito dentro Date, che esiste solo per avere un tipo che segnali questo preciso errore.

Eccezione. Meccanismo che separa il rilevamento di un errore (spesso in una funzione chiamata) dalla sua gestione (spesso nel chiamante). Una funzione che non può gestire l'errore non ritorna normalmente: lancia (throw) un oggetto; un chiamante, diretto o indiretto, lo cattura (catch) con un blocco try. Se nessuno lo gestisce, il programma termina. È l'unico modo in cui un costruttore può fallire. Dettagli in C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →.

date_final.cppcpp
#include <iostream>
using namespace std;

namespace Chrono {

class Date {
public:
    class Invalid {};                       // tipo vuoto: segnala "data non valida"

    Date(int yy, int mm, int dd);           // controlla la validita' e inizializza
    void add_day(int n);                    // avanza di n giorni (n >= 0)

    int year() const { return y; }          // funzioni const: non modificano l'oggetto
    int month() const { return m; }
    int day() const { return d; }

private:
    int y, m, d;
    static int days_in_month(int y, int m);
    bool is_valid() const;                  // verifica gli invarianti
};

bool leapyear(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; }

int Date::days_in_month(int y, int m)
{
    switch (m) {
    case 2:  return leapyear(y) ? 29 : 28;
    case 4: case 6: case 9: case 11: return 30;
    default: return 31;
    }
}

bool Date::is_valid() const
{
    if (m < 1 || m > 12) return false;
    return d >= 1 && d <= days_in_month(y, m);
}

Date::Date(int yy, int mm, int dd)
    : y{yy}, m{mm}, d{dd}
{
    if (!is_valid()) throw Invalid();       // un costruttore puo' fallire solo cosi'
}

void Date::add_day(int n)
{
    for (int i = 0; i < n; ++i) {
        if (++d > days_in_month(y, m)) {
            d = 1;
            if (++m > 12) { m = 1; ++y; }
        }
    }
}

// helper function: non hanno bisogno dei dati privati, usano l'interfaccia pubblica
bool operator==(const Date& a, const Date& b)
{
    return a.year() == b.year() && a.month() == b.month() && a.day() == b.day();
}

bool operator!=(const Date& a, const Date& b) { return !(a == b); }

ostream& operator<<(ostream& os, const Date& dt)
{
    return os << dt.year() << '-' << dt.month() << '-' << dt.day();
}

Date next_day(const Date& dt)
{
    Date r = dt;                            // copia
    r.add_day(1);
    return r;
}

}   // namespace Chrono

int main()
{
    using namespace Chrono;
    try {
        Date d {2023, 2, 27};
        cout << d << '\n';
        d.add_day(2);
        cout << "dopo 2 giorni: " << d << '\n';
        Date bis {2024, 2, 28};
        cout << "28/2/2024 + 1 giorno: " << next_day(bis) << '\n';
        cout << (next_day(bis) == Date{2024, 2, 29}) << '\n';
        cout << "Provo con 2023-2-30\n";
        Date wrong {2023, 2, 30};           // lancia Invalid
        cout << "non arrivo qui\n";
    }
    catch (Date::Invalid) {
        cout << "Invalid date!\n";
    }
    return 0;
}

Uscita (l'ultima riga viene dal ramo catch. In WebAssembly le eccezioni non funzionano: il programma è compilato per Linux e l'uscita è ricavata a mano, poi confermata eseguendo una copia in cui throw stampa e termina e try/catch sono tolti):

2023-2-27
dopo 2 giorni: 2023-3-1
28/2/2024 + 1 giorno: 2024-2-29
1
Provo con 2023-2-30
Invalid date!

La traccia del add_day: 27 febbraio 2023 + 1 giorno = 28; + 1 giorno = 29 > 28 giorni del mese →\to d = 1, m = 3: 2023-3-1. Nel 2024, bisestile, il 28 febbraio + 1 dà il 29. Date wrong {2023, 2, 30} rende is_valid() falsa (30 > 28), il costruttore lancia Invalid e l'oggetto non esiste.

Dove si scrivono le funzioni membro

Le funzioni membro vanno dichiarate nella classe e possono essere definite dentro o fuori.

dentro la classe (in-class definition) fuori dalla classe
sintassi corpo nella definizione della classe tipo Classe::nome(...) { ... }
effetto rende la funzione automaticamente inline (il codice è replicato a ogni chiamata) normale funzione
pregi utile per funzioni brevissime e chiamate spesso interfaccia pulita e corta
difetti classe più lunga e disordinata, dettagli implementativi mischiati all'interfaccia; ogni modifica impone di ricompilare tutto il codice che usa la classe meno compatto

Organizzazione consigliata: prima l'interfaccia, poi i dettagli:

cpp
class Date {
public:
    Date(int y, int m, int d);   // controlla validità e inizializza
    void add_day(int n);
    int month() const;
    int day() const;
    int year() const;
private:
    int y, m, d;
};

Buona pratica: non definire funzioni membro dentro la classe. Eccezione: la funzione è molto piccola e si vuole l'inlining. Una funzione con più di una o due espressioni non ne beneficia. inline è solo una richiesta al compilatore, che può ignorarla. Fuori dalla classe, il nome deve avere il prefisso Date::: scordarlo (int month()) definisce una funzione libera e m non è più in scope.

Funzioni membro const

Gli argomenti passati per reference o per const reference (C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri →) pongono un problema:

cpp
void some_function(Date& d, const Date& start_of_term)
{
    int a = d.day();              // ok
    int b = start_of_term.day();  // dovrebbe essere ok: legge soltanto
    d.add_day(3);                 // ok
    start_of_term.add_day(3);     // errore: modifica un oggetto const
}

Se day() non è dichiarata const, il compilatore non ha la garanzia che l'oggetto non sarà modificato (la funzione non lo modifica, ma non è esplicito). Errore reale:

error: 'this' argument to member function 'day' has type 'const Date', but function is not marked const

La soluzione è dichiarare const le funzioni membro che non modificano lo stato:

cpp
int day() const;       // const dopo la lista dei parametri
Month month() const;
void add_day(int n);   // modifica: non const

Modificare un membro dentro una funzione const dà un errore di compilazione (++d; in int Date::day() const). Quindi: identificare le funzioni membro costanti è una regola dell'interfaccia ben fatta.

Funzioni membro o helper function

Per alcune funzionalità si può scegliere fra funzione membro e helper function (funzione libera). Criterio:

"A function that can be simply, elegantly, and efficiently implemented as a freestanding function should be implemented outside the class. That way, a bug in that function cannot directly corrupt the data in a class object." (Stroustrup)

Se i dati membro risultano corrotti, solo le funzioni membro vanno controllate: meno sono, meglio sono protetti i dati. Una libreria di date commerciale può avere funzioni come next_sunday() e next_workday() per un totale di decine di funzioni: non è l'interfaccia minimale che si vuole.

Le helper function sono tipicamente operator==, operator!=, next_sunday, leapyear; usano l'interfaccia pubblica (d.day(), d.month(), d.year()), non i dati privati. Spesso stanno nello stesso namespace della classe (namespace Chrono, come nel programma sopra), così sono vicine alla classe senza farne parte (C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →).

Domande d'esame

1. Quali sono le due parti di una classe e che differenza c'è fra interfaccia e implementazione? Traccia. Interfaccia = parte pubblica, il punto di vista di chi usa la classe; implementazione = parte privata (dati e dettagli), accessibile solo tramite l'interfaccia, il che permette di mantenere lo stato coerente.

2. Perché per Date si usa un costruttore invece di una funzione init_day()? Traccia. Il costruttore è chiamato obbligatoriamente alla creazione dell'oggetto, quindi non esistono Date non inizializzate o inizializzate male; una funzione init_day() si può non chiamare o bypassare.

3. Che cos'è un invariante? Fare esempi. Traccia. Regola che definisce gli stati validi; il costruttore la stabilisce, le funzioni membro la mantengono. Date: mese in 1-12 e giorno compatibile col mese e con l'anno bisestile; Rational: denominatore ≠0\neq 0, frazione ridotta; vector: sz uguale alla dimensione del buffer.

4. Quando va definita una funzione dentro la classe e quando fuori? Traccia. Dentro solo se brevissima (1-2 espressioni) e da inlinare; fuori in tutti gli altri casi: interfaccia leggibile, meno ricompilazioni.

5. Che cosa significa dichiarare una funzione membro const? Traccia. Garantisce che non modifica l'oggetto; è necessaria per poterla chiamare su oggetti const o su const T&; la violazione è un errore di compilazione.

Versione ripasso

cpp
class Date {
public:
    Date(int y, int m, int d);       // controlla validità e inizializza
    void add_day(int n);
    int year() const; int month() const; int day() const;
private:
    int y, m, d;
    bool is_valid() const;
};
Date::Date(int yy, int mm, int dd) : y{yy}, m{mm}, d{dd}    // lista di inizializzazione
{ if (!is_valid()) throw Invalid(); }
cpp
namespace Chrono {
class Date {
public:
    class Invalid {};
    Date(int yy, int mm, int dd);          // lancia Invalid se la data non esiste
    void add_day(int n);
    int year() const { return y; }  int month() const { return m; }  int day() const { return d; }
private:
    int y, m, d;
    static int days_in_month(int y, int m);
    bool is_valid() const;
};
bool operator==(const Date& a, const Date& b)   // helper: usa solo l'interfaccia
{ return a.year() == b.year() && a.month() == b.month() && a.day() == b.day(); }
}
try { Date wrong {2023, 2, 30}; } catch (Date::Invalid) { cout << "Invalid date!\n"; }
  • Uscita del programma completo: 2023-2-27, dopo 2 giorni: 2023-3-1, 28/2/2024 + 1 giorno: 2024-2-29, 1 (uguaglianza vera), poi Invalid date!.
  • Domande tipiche: (1) due parti di una classe = interfaccia pubblica e implementazione privata; (2) costruttore invece di init_day() perché è obbligatorio alla creazione e non si può bypassare; (3) invariante = regola degli stati validi (esempi Date, Rational, vector: sz = dimensione del buffer); (4) funzioni dentro la classe solo se brevissime; (5) const = non modifica l'oggetto.
  • Versione 1, perché non basta: Date x; x.y = -3; x.m = 13; x.d = 32; compila; tomorrow.d = today.d + 1; può dare 32; l'helper init_day si può non chiamare.
  • Forme di costruzione: Date last {2000,12,31}; (consigliata), Date next = {2014,2,14};, Date christmas = Date{1976,12,24};, Date last(2000,12,31); (vecchio stile); last.add_day(1); ok, add_day(2); errore (quale data?).
  • Errore tipico del compilatore: start_of_term.add_day(3); con const Date& →\to 'this' argument to member function ... has type 'const Date', but function is not marked const.
  • Errori tipici: scordare Date:: fuori dalla classe; dimenticare const sulle funzioni di lettura; dati pubblici che invalidano l'invariante; costruttore che non controlla; mettere troppe funzioni nella classe; assegnare i membri nel corpo invece di usare la lista.

Esercizi su questo argomento

Teoria collegata