Esercizio - Helper function e la classe Book (terzo appello 2021-22, teoria)
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
Bookriportata 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.
- 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.
- Interfaccia minimale. La classe resta piccola e comprensibile: un'interfaccia con cinquanta funzioni come
next_sunday(),next_workday()è difficile da capire, implementare e mantenere. - Chiarezza e flessibilità: gli operatori binari come helper function accettano conversioni sui due operandi (
int + RationaleRational + int) e rendono simmetrico il confronto. - 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.
#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;friendda evitare. - Soluzione per
Book:
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;lend1, poi 0;in prestito;uguali? 1, 0;available_by(..., "Eco")soloIl 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 iBookper 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.