Vai al contenuto

Costruttori, Distruttori e Regola dello 0/3/5

Padroneggia l'intero ciclo di vita degli oggetti C++ — dalla costruzione alla distruzione — e impara quando scrivere (o non scrivere) le funzioni membro speciali.

Il Ciclo di Vita degli Oggetti in C++

In C++ hai il controllo diretto su quando e come gli oggetti vengono creati e distrutti. Il compilatore può generare automaticamente fino a sei funzioni membro speciali: costruttore predefinito, costruttore di copia, operatore di assegnamento per copia, costruttore di spostamento, operatore di assegnamento per spostamento e distruttore. Capire quando scriverle tu — e quando lasciar fare al compilatore — è una delle abilità più importanti in C++.

Tipi di Costruttori

I costruttori inizializzano un oggetto. Il C++ ne supporta diversi tipi: predefinito (senza argomenti), parametrizzato, di conversione (argomento singolo, conversione implicita) e delegante (un costruttore ne chiama un altro).

constructors.cpp
#include <iostream>
#include <string>

class Temperature {
    double celsius_;
    std::string label_;

public:
    // Default constructor
    Temperature() : celsius_{0.0}, label_{"unknown"} {}

    // Parameterized constructor
    Temperature(double c, const std::string& label)
        : celsius_{c}, label_{label} {}

    // Converting constructor — allows implicit conversion from double
    Temperature(double c) : celsius_{c}, label_{"sensor"} {}

    // Delegating constructor — reuses another constructor
    Temperature(const std::string& label)
        : Temperature{0.0, label} {}  // delegates to parameterized

    void print() const {
        std::cout << label_ << ": " << celsius_ << "°C\n";
    }
};

int main() {
    Temperature t1;                   // default
    Temperature t2{36.6, "body"};     // parameterized
    Temperature t3 = 100.0;           // converting (implicit)
    Temperature t4{"outdoor"};        // delegating

    t1.print();  // unknown: 0°C
    t2.print();  // body: 36.6°C
    t3.print();  // sensor: 100°C
    t4.print();  // outdoor: 0°C
    return 0;
}

La Parola Chiave explicit

I costruttori a un solo argomento abilitano le conversioni implicite, che possono causare bug sottili. Usa explicit per impedire conversioni indesiderate — è quasi sempre ciò che vuoi.

explicit.cpp
#include <iostream>
#include <string>

class UserId {
    int id_;
public:
    explicit UserId(int id) : id_{id} {}
    int get() const { return id_; }
};

void process(UserId uid) {
    std::cout << "Processing user #" << uid.get() << '\n';
}

int main() {
    UserId u1{42};       // OK: direct initialization
    // UserId u2 = 42;   // ERROR: implicit conversion blocked by explicit
    // process(42);      // ERROR: would require implicit conversion
    process(UserId{42}); // OK: explicit construction
    return 0;
}

Liste di Inizializzazione dei Membri e Ordine di Inizializzazione

La lista di inizializzazione dei membri è il modo preferito per inizializzare i membri. È obbligatoria per i membri const, per i membri riferimento e per i membri privi di costruttore predefinito. I membri vengono inizializzati nell'ordine di dichiarazione, non nell'ordine in cui compaiono nella lista di inizializzazione.

init_list.cpp
#include <iostream>
#include <string>

class Connection {
    const int max_retries_;      // const: MUST use init list
    std::string& log_target_;   // reference: MUST use init list
    std::string host_;
    int port_;

public:
    Connection(std::string& log, const std::string& host, int port)
        : max_retries_{3}          // const member
        , log_target_{log}         // reference member
        , host_{host}              // initialized in declaration order
        , port_{port}
    {
        // Constructor body runs AFTER all members are initialized
        log_target_ += "[Connection to " + host_ + " created]\n";
    }

    void print() const {
        std::cout << host_ << ':' << port_
                  << " (max retries: " << max_retries_ << ")\n";
    }
};

int main() {
    std::string log;
    Connection conn{log, "db.example.com", 5432};
    conn.print();
    std::cout << log;
    return 0;
}

Distruttori e Regola dello Zero / del Tre / del Cinque

Un distruttore (~ClassName()) viene eseguito quando un oggetto viene distrutto — rilascia le risorse e non deve lanciare eccezioni. Per la maggior parte delle classi, il distruttore generato dal compilatore è sufficiente.

La Regola dello Zero: preferisci wrapper RAII (std::string, std::vector, std::unique_ptr) così da non dover mai scrivere alcuna funzione membro speciale. Se devi gestire una risorsa grezza, la Regola del Tre dice che ti servono un distruttore, un costruttore di copia e un operatore di assegnamento per copia. La Regola del Cinque estende questo elenco includendo il costruttore di spostamento e l'operatore di assegnamento per spostamento.

rule_of_five.cpp
#include <iostream>
#include <cstring>

// === Rule of Zero: the GOAL ===
// No special members needed — std::string handles everything
class Person {
    std::string name_;
    int age_;
public:
    Person(const std::string& name, int age)
        : name_{name}, age_{age} {}
    // Compiler generates correct copy/move/destructor automatically!
};

// === Rule of Five: when managing a raw resource ===
class CharBuffer {
    char* data_;
    std::size_t size_;

public:
    explicit CharBuffer(const char* str)
        : size_{std::strlen(str)}
        , data_{new char[std::strlen(str) + 1]}
    {
        std::strcpy(data_, str);
    }

    // Destructor
    ~CharBuffer() { delete[] data_; }

    // Copy constructor (deep copy)
    CharBuffer(const CharBuffer& other)
        : size_{other.size_}
        , data_{new char[other.size_ + 1]}
    {
        std::strcpy(data_, other.data_);
    }

    // Copy assignment (copy-and-swap idiom)
    CharBuffer& operator=(CharBuffer other) {  // note: by value
        swap(*this, other);
        return *this;
    }

    // Move constructor
    CharBuffer(CharBuffer&& other) noexcept
        : data_{other.data_}, size_{other.size_}
    {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // Move assignment
    CharBuffer& operator=(CharBuffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    friend void swap(CharBuffer& a, CharBuffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    void print() const {
        std::cout << (data_ ? data_ : "(null)") << '\n';
    }
};

int main() {
    CharBuffer a{"Hello"};
    CharBuffer b = a;           // copy constructor
    CharBuffer c{"World"};
    c = a;                      // copy assignment
    CharBuffer d = std::move(a); // move constructor
    d.print();  // Hello
    a.print();  // (null) — moved-from state
    return 0;
}

=default e =delete

Puoi richiedere esplicitamente la versione generata dal compilatore di un membro speciale con =default, oppure sopprimerla esplicitamente con =delete. Questo rende chiare le tue intenzioni e documenta la semantica di copia e spostamento della classe.

default_delete.cpp
#include <memory>
#include <string>

// Non-copyable, movable resource handle
class FileHandle {
    std::string path_;
    int fd_;

public:
    FileHandle(const std::string& path, int fd)
        : path_{path}, fd_{fd} {}

    // Explicitly default the destructor
    ~FileHandle() = default;

    // Delete copy operations — file handles should not be duplicated
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Default move operations
    FileHandle(FileHandle&&) = default;
    FileHandle& operator=(FileHandle&&) = default;
};

int main() {
    FileHandle fh{"data.txt", 3};
    // FileHandle copy = fh;         // ERROR: copy deleted
    FileHandle moved = std::move(fh); // OK: move is allowed
    return 0;
}
Insidia

I membri vengono inizializzati nell'ordine in cui sono dichiarati nella classe, non nell'ordine in cui compaiono nella lista di inizializzazione. Se un membro dipende da un altro, assicurati che la dipendenza sia dichiarata per prima. I compilatori avvisano di questo con -Wreorder — non ignorare mai quel warning. Un bug classico: inizializzare size_ da un parametro e poi usare size_ per allocare data_, con data_ dichiarato prima di size_.

Concetti chiave
  • Usa explicit sui costruttori a un solo argomento per impedire conversioni implicite indesiderate
  • La lista di inizializzazione dei membri è obbligatoria per i membri const e riferimento, ed è più efficiente per tutti i membri
  • I membri si inizializzano nell'ordine di dichiarazione, non in quello della lista di inizializzazione
  • Punta alla Regola dello Zero — usa tipi RAII così che i membri speciali generati dal compilatore siano corretti
  • Se scrivi uno tra distruttore, costruttore di copia e assegnamento per copia, scrivili tutti e cinque (Regola del Cinque)
  • =default documenta l'intenzione; =delete vieta esplicitamente un'operazione

Quiz — Metti alla prova le tue conoscenze

(20 XP)

1. Cosa impedisce la parola chiave `explicit` su un costruttore?

2. In quale ordine vengono inizializzati i membri di una classe?

3. Che cos'è la Regola dello Zero?