Salta al contenuto
Note per Studenti Esercizio - Helper function e la classe Book (terzo appello 2021-22, teoria)

Esercizio - Helper function e la classe Book (terzo appello 2021-22, teoria)

Esame

Domanda aperta 1 del terzo appello 2021-22 (30 giugno 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: C++ - classi, membri e invariantiuna classe è un tipo definito dal programmatore che rappresenta un concetto con una rappresentazione (dati membro) e delle operazioni (funzioni membro); l'interfaccia pubblica separa chi usa la classe da chi la implementa e i dati privati sono modificabili solo tramite l'interfaccia; struct è una class con membri pubblici di default; l'invariante è la regola che definisce gli stati validi, e il costruttore deve stabilirlo e ogni funzione membro conservarlo; il costruttore (stesso nome della classe) inizializza con una lista di inizializzazione e, se i dati non sono validi, lancia un'eccezione, l'unico modo per far fallire un costruttore; le funzioni membro si definiscono fuori dalla classe (solo le brevissime dentro, dove sono inline); le funzioni che non modificano l'oggetto sono dichiarate const; le funzioni che si possono scrivere con la sola interfaccia pubblica (operator==, next_sunday) sono helper function esterne, spesso nello stesso namespace.C++ - classi, membri e invarianti →, 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 →.

Testo. Descrivere approfonditamente cosa sono le helper function evidenziandone i vantaggi. Si consideri la classe Book riportata di seguito. Scrivere il codice C++ di almeno 2 helper function per questa classe.

#include <string>
class Book {
public:
    Book(const std::string& title, const std::string& author);
    std::string title(void) const;
    std::string author(void) const;
    void set_title(const std::string& title);
    void set_author(const std::string& author);
    void set_availability(bool available);
    bool is_available(void) const;
private:
    std::string title_;
    std::string author_;
    bool available_;
};

Che cosa sono le helper function

Helper function. Funzione esterna alla classe (non è una funzione membro) che aiuta a lavorare con gli oggetti della classe e ne usa solo l'interfaccia pubblica. Spesso ha argomenti del tipo della classe (non è una regola) e sta nello stesso namespace della classe.

Esempi tipici: operator== e operator!=, operator<<, next_sunday(const Date&), leapyear(int).

Vantaggi.

  1. Protezione dei dati. Il principio è quello di Stroustrup: una funzione che si può scrivere in modo semplice, elegante ed efficiente come funzione libera va scritta fuori dalla classe: un bug in quella funzione non può corrompere direttamente i dati dell'oggetto. Se lo stato è corrotto, bisogna controllare solo le funzioni membro: meno sono, più sono protetti i dati.
  2. Interfaccia minimale. La classe resta piccola e comprensibile: un'interfaccia con cinquanta funzioni come next_sunday(), next_workday() è difficile da capire, implementare e mantenere.
  3. Chiarezza e flessibilità: gli operatori binari come helper function accettano conversioni sui due operandi (int + Rational e Rational + int) e rendono simmetrico il confronto.
  4. Manutenzione. Cambiare una helper function non obbliga a ricompilare i client della classe (non è nella definizione della classe) e non può rompere l'invariante.

Svantaggio / limite: non vedono i membri privati, quindi per ciò che richiede l'accesso allo stato (cambiarlo, o leggerlo in modi non previsti) serve una funzione membro; è questo il motivo per cui l'uso di friend va evitato quando possibile.

Soluzione: quattro helper function per Book

Si usano solo title(), author(), is_available() e set_availability() (l'interfaccia pubblica). Il programma completa Book con una realizzazione minima per provarle.

book_helper.cppcpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;

// La classe data dal testo (dichiarazione), con una realizzazione minima per poter provare le helper function.
class Book {
public:
    Book(const std::string& title, const std::string& author);
    std::string title(void) const;
    std::string author(void) const;
    void set_title(const std::string& title);
    void set_author(const std::string& author);
    void set_availability(bool available);
    bool is_available(void) const;
private:
    std::string title_;
    std::string author_;
    bool available_;
};

Book::Book(const string& title, const string& author)
    : title_{title}, author_{author}, available_{true} {}
string Book::title(void) const { return title_; }
string Book::author(void) const { return author_; }
void Book::set_title(const string& title) { title_ = title; }
void Book::set_author(const string& author) { author_ = author; }
void Book::set_availability(bool available) { available_ = available; }
bool Book::is_available(void) const { return available_; }

// ---- HELPER FUNCTION: sono fuori dalla classe e usano SOLO l'interfaccia pubblica ----

// 1) uguaglianza: stesso titolo e stesso autore (la disponibilita' non fa parte dell'identita' del libro)
bool operator==(const Book& a, const Book& b)
{
    return a.title() == b.title() && a.author() == b.author();
}
bool operator!=(const Book& a, const Book& b) { return !(a == b); }

// 2) stampa su uno stream: "Titolo (Autore) - disponibile / in prestito"
ostream& operator<<(ostream& os, const Book& b)
{
    return os << b.title() << " (" << b.author() << ") - "
              << (b.is_available() ? "disponibile" : "in prestito");
}

// 3) prestito: se il libro e' disponibile lo segna in prestito e dice se ci e' riuscita
bool lend(Book& b)
{
    if (!b.is_available()) return false;
    b.set_availability(false);
    return true;
}

// 4) ricerca: tutti i libri disponibili di un autore
vector<Book> available_by(const vector<Book>& library, const string& author)
{
    vector<Book> found;
    for (const Book& b : library)
        if (b.author() == author && b.is_available()) found.push_back(b);
    return found;
}

int main()
{
    vector<Book> library {
        Book("Il nome della rosa", "Eco"),
        Book("Il pendolo di Foucault", "Eco"),
        Book("Se questo e' un uomo", "Levi")
    };
    cout << library[0] << '\n';
    cout << "lend(library[0]): " << lend(library[0]) << ", di nuovo: " << lend(library[0]) << '\n';
    cout << library[0] << '\n';
    cout << "uguali? " << (library[0] == Book("Il nome della rosa", "Eco")) << ", "
         << (library[0] == library[1]) << '\n';
    for (const Book& b : available_by(library, "Eco")) cout << "disponibile di Eco: " << b << '\n';
    return 0;
}
Il nome della rosa (Eco) - disponibile
lend(library[0]): 1, di nuovo: 0
Il nome della rosa (Eco) - in prestito
uguali? 1, 0
disponibile di Eco: Il pendolo di Foucault (Eco) - disponibile
helper function scopo note
operator==(const Book&, const Book&) e operator!= uguaglianza: stesso titolo e stesso autore la disponibilità non fa parte dell'identità; per const reference (due string da non copiare)
operator<<(ostream&, const Book&) stampa Titolo (Autore) - disponibile/in prestito restituisce lo stream per concatenare
lend(Book&) segna il libro in prestito se disponibile e dice se ci è riuscita reference non const perché modifica il libro, ma tramite set_availability
available_by(const vector<Book>&, const string&) libri disponibili di un autore lavora su una collezione di libri

Perché sono helper e non membri: nessuna richiede i dati privati (title_, author_, available_); se operator== avesse un bug, non potrebbe rovinare l'oggetto: lo leggerebbe soltanto. Provando a scrivere nella helper return a.title_ == b.title_; il compilatore risponde 'title_' is a private member of 'Book'.

Versione ripasso

  • Helper function = funzione esterna alla classe, che usa solo l'interfaccia pubblica; spesso nello stesso namespace (es. operator==, operator<<, next_sunday, leapyear).
  • Vantaggi: (1) un bug non può corrompere direttamente i dati (Stroustrup): se lo stato è corrotto si controllano solo le funzioni membro; (2) interfaccia minimale (non cinquanta funzioni come next_workday); (3) operatori binari con conversioni su entrambi gli operandi; (4) niente ricompilazione dei client. Limite: non vedono i membri privati; friend da evitare.
  • Soluzione per Book:
cpp
bool operator==(const Book& a, const Book& b)
{ return a.title() == b.title() && a.author() == b.author(); }     // la disponibilita' non conta
bool operator!=(const Book& a, const Book& b) { return !(a == b); }
ostream& operator<<(ostream& os, const Book& b)
{ return os << b.title() << " (" << b.author() << ") - " << (b.is_available() ? "disponibile" : "in prestito"); }
bool lend(Book& b) { if (!b.is_available()) return false; b.set_availability(false); return true; }
vector<Book> available_by(const vector<Book>& lib, const string& author);   // disponibili di un autore
  • Prova: Il nome della rosa (Eco) - disponibile; lend →\to 1, poi 0; in prestito; uguali? 1, 0; available_by(..., "Eco") →\to solo Il pendolo di Foucault.
  • Errori tipici: scrivere queste funzioni come membri (e gonfiare la classe); accedere a title_ ('title_' is a private member of 'Book'); passare i Book per valore; operator<< che non restituisce lo stream.
  • Perché solo l'interfaccia pubblica: scrivendo return a.title_ == b.title_; il compilatore risponde 'title_' is a private member of 'Book'; le helper function vedono quanto vede ogni utente della classe, quindi non possono rompere l'invariante.

Esercizi su questo argomento

Teoria collegata