Laboratorio 06

Regola del Tre, Polimorfismo e SequentialNetwork

Docente

Soluzione del Laboratorio 05

La classe Matrix con dati privati e indicizzazione row-major:

Matrix::Matrix(int r, int c) : rows(r), cols(c) {
    if (r <= 0 || c <= 0)
        throw std::invalid_argument("Le dimensioni della matrice devono essere > 0");
    data = new double[rows * cols];
    for (int i = 0; i < rows * cols; ++i) data[i] = 0.0;
}

double Matrix::get(int r, int c) const {
    if (r < 0 || r >= rows || c < 0 || c >= cols)
        throw std::out_of_range("Indici di matrice fuori dai limiti!");
    return data[r * cols + c];      // row-major: una 2D in un array 1D
}

E il DenseLayer che invece delega la memoria a Eigen:

DenseLayer::DenseLayer(int input_size, int output_size) {
    weights = Eigen::MatrixXd::Random(output_size, input_size);
    biases  = Eigen::MatrixXd::Zero(output_size, 1);
}

Eigen::MatrixXd DenseLayer::forward(const Eigen::MatrixXd& input) {
    return (weights * input).colwise() + biases.col(0);   // broadcasting
}

Restava aperta la bomba: Matrix alloca con new[] e non ha distruttore. Oggi la disinneschiamo.

Obiettivo di Oggi

Due argomenti che si intrecciano:

  1. La Regola del Tre: se una classe possiede memoria cruda, servono distruttore, costruttore di copia e operatore di assegnazione. La applicheremo due volte, su Matrix e su SequentialNetwork.
  2. Il polimorfismo: un’interfaccia ILayer che permetta a DenseLayer e ai layer di attivazione di stare nello stesso contenitore.

Task 1: La Regola del Tre su Matrix

Aggiungete a matrix.hpp i tre membri che mancano:

class Matrix {
private:
    int rows, cols;
    double* data;
public:
    Matrix(int r, int c);

    // ===== LA REGOLA DEL 3 =====
    ~Matrix();                                  // 1. Distruttore
    Matrix(const Matrix& other);                // 2. Costruttore di Copia
    Matrix& operator=(const Matrix& other);     // 3. Operatore di Assegnazione

    double get(int r, int c) const;
    void set(int r, int c, double val);
    int getRows() const;
    int getCols() const;
};

Task 2: Implementare i tre

// 1. Distruttore
Matrix::~Matrix() {
    delete[] data;
}

// 2. Costruttore di Copia — DEEP copy: nuova memoria, dati clonati
Matrix::Matrix(const Matrix& other) : rows(other.rows), cols(other.cols) {
    data = new double[rows * cols];
    for (int i = 0; i < rows * cols; ++i)
        data[i] = other.data[i];
}

// 3. Operatore di Assegnazione
Matrix& Matrix::operator=(const Matrix& other) {
    if (this == &other) return *this;     // a. auto-assegnazione: m1 = m1

    delete[] data;                        // b. libera la vecchia memoria

    rows = other.rows;                    // c. copia campi e rialloca
    cols = other.cols;
    data = new double[rows * cols];
    for (int i = 0; i < rows * cols; ++i)
        data[i] = other.data[i];

    return *this;                         // d. riferimento a se stesso
}

I quattro passi dell’operatore = vanno in quest’ordine: invertire (b) e (a) su m1 = m1 distrugge i dati che si stanno per copiare.

Task 3: L’interfaccia ILayer

Perché la rete possa contenere layer di tipi diversi, serve una base comune. Crea ilayer.hpp:

#ifndef ILAYER_HPP
#define ILAYER_HPP

#include <Eigen/Dense>

class ILayer {
public:
    // Distruttore virtuale: obbligatorio, si farà delete su puntatori a ILayer
    virtual ~ILayer() = default;

    // Virtuale puro: rende ILayer astratta (non istanziabile)
    virtual Eigen::MatrixXd forward(const Eigen::MatrixXd& input) = 0;

    // Idioma del "Costruttore Virtuale": serve alla Deep Copy polimorfica
    virtual ILayer* clone() const = 0;
};

#endif

Poi fate derivare DenseLayer da ILayer, aggiungendo override a forward e implementando clone():

ILayer* DenseLayer::clone() const {
    return new DenseLayer(*this);    // usa il copy constructor generato da Eigen
}

Important

clone() non è opzionale: il costruttore di copia di SequentialNetwork non può funzionare senza. Da un ILayer* non si sa quale tipo concreto copiare — solo l’oggetto stesso lo sa.

Task 4: SequentialNetwork — l’header

Crea sequential_network.hpp. Il contenitore è un array C-style dinamico: serve a farvi toccare i problemi che std::vector risolverà da solo.

#ifndef SEQUENTIAL_NETWORK_HPP
#define SEQUENTIAL_NETWORK_HPP

#include "ilayer.hpp"
#include <stdexcept>

class SequentialNetwork {
private:
    ILayer** layers;        // array dinamico di puntatori a ILayer
    int capacity;
    int size;

public:
    SequentialNetwork(int max_layers);

    // ===== LA REGOLA DEL 3 (di nuovo) =====
    ~SequentialNetwork();
    SequentialNetwork(const SequentialNetwork& other);
    SequentialNetwork& operator=(const SequentialNetwork& other);

    void add(ILayer* layer);
    Eigen::MatrixXd forward(const Eigen::MatrixXd& input);
    int getSize() const;
};

#endif

Task 5: SequentialNetwork — l’implementazione

SequentialNetwork::SequentialNetwork(int max_layers)
    : capacity(max_layers), size(0) {
    if (capacity <= 0) throw std::invalid_argument("Capacity deve essere > 0");
    layers = new ILayer*[capacity];
    for (int i = 0; i < capacity; ++i) layers[i] = nullptr;
}

SequentialNetwork::~SequentialNetwork() {
    for (int i = 0; i < size; ++i) delete layers[i];   // a. i layer
    delete[] layers;                                   // b. l'array
}

SequentialNetwork::SequentialNetwork(const SequentialNetwork& other)
    : capacity(other.capacity), size(other.size) {
    layers = new ILayer*[capacity];
    for (int i = 0; i < size; ++i)
        layers[i] = other.layers[i]->clone();          // clone POLIMORFICO
}

void SequentialNetwork::add(ILayer* layer) {
    if (size >= capacity) throw std::out_of_range("Network capacity esaurita!");
    layers[size++] = layer;
}

Eigen::MatrixXd SequentialNetwork::forward(const Eigen::MatrixXd& input) {
    Eigen::MatrixXd current = input;
    for (int i = 0; i < size; ++i)
        current = layers[i]->forward(current);   // ogni output è il prossimo input
    return current;
}

L’operator= segue lo stesso schema di Matrix: auto-assegnazione, libera, rialloca, clona. Attenzione che qui il “libera” è doppio: prima i layer, poi l’array.

Task 6: Il Test della Deep Copy

#include <iostream>
#include <Eigen/Dense>
#include "dense_layer.hpp"
#include "sequential_network.hpp"

int main() {
    Eigen::MatrixXd X = Eigen::MatrixXd::Random(784, 1);

    SequentialNetwork net1(2);
    // La rete fa delete sui puntatori: passiamo oggetti creati con new
    net1.add(new DenseLayer(784, 128));
    net1.add(new DenseLayer(128, 10));

    Eigen::MatrixXd Z1 = net1.forward(X);
    std::cout << "Rete 1:\n" << Z1.transpose() << std::endl;

    SequentialNetwork net2 = net1;          // <- Costruttore di Copia
    Eigen::MatrixXd Z2 = net2.forward(X);
    std::cout << "Rete 2 (copia):\n" << Z2.transpose() << std::endl;
}

Il criterio di correttezza: Z1 e Z2 devono essere identici. I pesi sono casuali, quindi due reti costruite da zero darebbero numeri diversi: se coincidono, il clone() ha davvero duplicato i pesi.

Provate poi a commentare il copy constructor: il programma crasherà con un Double Free, perché entrambe le reti farebbero delete sugli stessi layer.

Task da Completare

La soluzione di riferimento verrà discussa all’inizio della prossima lezione.