C++: make e makefile

Make è lo strumento che da decenni automatizza la compilazione dei programmi, e non solo: ogni volta che avete una serie di passi da eseguire che dipendono l’uno dall’altro, un Makefile fa al caso vostro. In questa puntata partiamo dalle basi, con Makefile minuscoli che possiamo provare subito, e arriviamo a un progetto C++ completo con struttura MVC.

Il makefile più semplice DEL mondo

Un Makefile è un file di testo, chiamato proprio Makefile, che contiene delle regole. Ogni regola ha un target (il nome di quello che vogliamo ottenere), i suoi prerequisiti e i comandi da eseguire. Attenzione: le righe dei comandi devono cominciare con un TAB, non con degli spazi. Se usate gli spazi, make vi risponde con un errore antipatico (missing separator).

La regola più semplice si chiama all e stampa una scritta:

all:
	echo "Hello World"

Lanciamo make senza argomenti. Make mostra il comando che esegue e poi il suo output:

make

Ecco che cosa vediamo:

echo "Hello World"
Hello World

Senza argomenti, make esegue il primo target del file, per questo di solito lo si chiama all.

I prerequisiti

Un target può dipendere da altri target. Prima di eseguire all, make esegue hello, perché è un suo prerequisito:

all: hello
	echo "World"

hello:
	echo "Hello"

Come vediamo dall’output, hello viene eseguito prima di all:

echo "Hello"
Hello
echo "World"
World

Il prerequisito può essere anche un file. Questo Makefile dice che per fare prova serve il file ciao.txt:

prova: ciao.txt
	echo "ok"

Se il file ciao.txt non esiste e non c’è nessuna regola per crearlo, make si ferma con un errore:

make: *** No rule to make target 'ciao.txt', needed by 'prova'.  Stop.

Se invece il file c’è, per esempio dopo un touch ciao.txt, il comando viene eseguito e vediamo:

echo "ok"
ok

Possiamo anche insegnare a make a creare il file. Qui la regola ciao.txt lo genera con un redirect, e all lo legge con cat:

all: ciao.txt
	cat ciao.txt

ciao.txt:
	echo "Hello World" > ciao.txt

La prima volta make crea il file e poi lo stampa:

echo "Hello World" > ciao.txt
cat ciao.txt
Hello World

Rilanciando make il file esiste già, quindi la regola ciao.txt non viene rieseguita, e vediamo solo la cat. È questo il meccanismo che fa ricompilare solo ciò che è cambiato: un target viene rifatto solo se manca o se è più vecchio di uno dei suoi prerequisiti.

Le variabili

Per non ripetere le stesse stringhe si usano le variabili. Si assegnano con il segno uguale e si usano con il dollaro e le parentesi:

T = hello

all:
	echo "$(T) mondo"
	echo "$(T)"

Il comando make stampa il valore di default:

hello mondo
hello

La cosa bella è che si può sovrascrivere dalla riga di comando, senza modificare il file:

make T=test

Questa volta l’output cambia:

test mondo
test

Un PROGETTO vero: la rubrica MVC

Passiamo a un caso reale. Il progetto anagraficaMVC4 è una piccola rubrica scritta in C++ con il pattern MVC: il Model contiene i dati (la classe anagrafica), la View si occupa di mostrarli e di chiedere l’input, il Controller coordina le due cose. Tre responsabilità separate, ognuna nel suo file. Il main ha solo il compito di partire.

Il Makefile cerca da solo tutti i sorgenti nelle sottocartelle, genera i file di dipendenza (.d) in modo che un cambiamento in un header faccia ricompilare chi lo include, e ha comodi target accessori: run per lanciare il programma, dist per impacchettarlo e clean per ripulire.

CC		= g++
CFLAGS		= -g -Wall
TARGET		= rubrica
LIBS		=
RM		= rm -f
DFLAGS		= -MM

SUBDIRS		:= $(shell ls -F | grep "\/" )
DIRS		:= ./ $(SUBDIRS)
SOURCES		:= $(foreach d, $(DIRS), $(wildcard $(d)*.cpp) )
OBJECTS		= $(patsubst %.cpp, %.o, $(SOURCES))
DEPENDENCIES	= $(patsubst %.cpp, %.d, $(SOURCES))

.PHONY		: dist clean run

%.d		: %.cpp
	$(CC) $(DFLAGS) $< -MT "$*.o $*.d" -MF $*.d

all		: $(DEPENDENCIES) $(TARGET)

$(TARGET)	: $(OBJECTS)
	$(CC) $(CFLAGS) -o $(TARGET) $(OBJECTS) $(LIBS)

ifneq "$(strip $(DEPENDENCIES))" ""
include $(DEPENDENCIES)
endif

%.o		: %.cpp
	$(CC) -c $(CFLAGS) -o $@ $<

clean		:
	$(RM) *.o
	$(RM) *.d
	$(RM) *.tar.gz

dist		: clean
	tar -cvzf rubrica.tar.gz *.cpp *.h README rubrica

run		: all clean
	./$(TARGET)

Il Model, cioè la classe anagrafica:

#include <string>

using namespace std;

#ifndef ANAGRAFICA_H
#define ANAGRAFICA_H

struct anagraficaDati {

    anagraficaDati() : nome(""), mail("") {}
    anagraficaDati( string nome , string mail )
        : nome(nome), mail(mail) {}

    string nome;
    string mail;

};

class anagrafica {

    public:
    anagrafica();
    string leggi_nome();
    string leggi_mail();
    void scrivi_nome( string nome );
    void scrivi_mail( string mail );

    private:
    string nome;
    string mail;

};

#endif

e la sua implementazione:

#include "anagrafica.h"

anagrafica::anagrafica() {
}

void anagrafica::scrivi_nome( string nome ) {

    this->nome = nome;

}

void anagrafica::scrivi_mail( string mail ) {

    this->mail = mail;

}

string anagrafica::leggi_nome() {

    return this->nome;

}

string anagrafica::leggi_mail() {

    return this->mail;

}

La View, con l’interfaccia verso l’utente:

#include <iostream>
#include <cstdlib>
#include <string>
#include <vector>

#include "anagrafica.h"

using namespace std;

#ifndef ANAGRAFICA_VIEW_H
#define ANAGRAFICA_VIEW_H

class anagraficaView {

    public:
    anagraficaView();
    anagraficaDati inserimento_anagrafica(
        anagraficaDati dati = anagraficaDati()
    );
    int modifica_anagrafica();
    int cancellazione_anagrafica();
    void elenco_anagrafiche( vector<anagrafica> lista );

    private:
    int richiesta_id();

};

#endif

#include "anagraficaView.h"

anagraficaView::anagraficaView() {
}

anagraficaDati anagraficaView::inserimento_anagrafica(
    anagraficaDati dati
) {

    cout << "inserimento dati anagrafica" << endl;

    cout << "inserisci nome: " << dati.nome << " ";
    getline( cin, dati.nome );

    cout << "inserisci mail: " << dati.mail << " ";
    getline( cin, dati.mail );

    return dati;

}

int anagraficaView::modifica_anagrafica() {

    cout << "modifica anagrafica" << endl;

    return this->richiesta_id();

}

int anagraficaView::cancellazione_anagrafica() {

    cout << "cancellazione anagrafica" << endl;

    return this->richiesta_id();

}

int anagraficaView::richiesta_id() {

    string id;

    cout << "inserisci l'id: ";
    getline( cin, id );

    return atoi( id.c_str() );

}

void anagraficaView::elenco_anagrafiche(
    vector<anagrafica> lista
) {

    cout << "elenco anagrafiche" << endl;

    for( unsigned i = 0; i < lista.size(); i++ ) {
        cout << i << "\t" << lista.at(i).leggi_nome()
             << "\t<" << lista.at(i).leggi_mail() << ">"
             << endl;
    }

}

Il Controller, che fa da collante:

#include <iostream>
#include <string>
#include <vector>

#include "anagrafica.h"
#include "anagraficaView.h"

using namespace std;

#ifndef ANAGRAFICA_CONTROLLER_H
#define ANAGRAFICA_CONTROLLER_H

class anagraficaController {

    public:
    anagraficaController();
    void inserimento_anagrafica();
    void modifica_anagrafica();
    void cancellazione_anagrafica();
    void elenco_anagrafiche();

    private:
    anagraficaView aView;
    vector<anagrafica> anagrafiche;

};

#endif

#include "anagraficaController.h"

anagraficaController::anagraficaController() {

    anagraficaView aView;

    // qui dovrei caricare le anagrafiche dal db?
    // e aggiungerle al vector?

}

void anagraficaController::inserimento_anagrafica() {

    anagrafica persona;
    anagraficaDati dati = aView.inserimento_anagrafica();

    persona.scrivi_nome( dati.nome );
    persona.scrivi_mail( dati.mail );

    // qui dovrei chiamare un metodo tipo persona.salva()
    // per scrivere la persona sul db?

    anagrafiche.push_back( persona );

}

void anagraficaController::modifica_anagrafica() {

    int id = aView.modifica_anagrafica();

    anagraficaDati datiAttuali(
    anagrafiche.at( id ).leggi_nome()
    ,
    anagrafiche.at( id ).leggi_mail()
    );

    anagraficaDati dati =
        aView.inserimento_anagrafica( datiAttuali );

    anagrafiche.at( id ).scrivi_nome( dati.nome );
    anagrafiche.at( id ).scrivi_mail( dati.mail );

    // qui dovrei chiamare un metodo tipo
    // anagrafiche.at( id ).salva() per scrivere sul db?

}

void anagraficaController::cancellazione_anagrafica() {

    int id = aView.cancellazione_anagrafica();

    // come cancello l'anagrafica dalla memoria e dal db?
    // dove effettuo le procedure pre e post cancellazione?

    anagrafiche.erase( anagrafiche.begin() + id );

}

void anagraficaController::elenco_anagrafiche() {

    aView.elenco_anagrafiche( anagrafiche );

}

E infine il main, brevissimo:

/**
 * PROGETTO DIMOSTRATIVO DEL PATTERN MVC
 *
 * @author Fabio "sean_von_drake" Mosti <fabio.mosti@gmail.com>
 */
#include <iostream>
#include <string>
#include <vector>

#include "anagraficaController.h"

using namespace std;

int main( void ) {

    string comando;

    anagraficaController aController;

    do {

    cout << "(i)nserisci (v)isualizza (m)odifica "
             << "(c)ancella (e)sci" << endl;
    cout << "comando: ";
    getline( cin, comando );

    if( comando.compare("i") == 0 ) {
        aController.inserimento_anagrafica();
    } else if( comando.compare("v") == 0 ) {
        aController.elenco_anagrafiche();
    } else if( comando.compare("m") == 0 ) {
        aController.modifica_anagrafica();
    } else if( comando.compare("c") == 0 ) {
        aController.cancellazione_anagrafica();
    }

    } while( ! comando.compare("e") == 0 );

    return 0;

}

Per compilare e provare basta:

make
make run

Nota: per far stare i listati nelle righe di questo articolo ho spezzato qualche riga lunga e sostituito i TAB con spazi nei sorgenti; il comportamento è identico. Nel Makefile, invece, le righe dei comandi vanno sempre rimesse con il TAB.

Per concludere

Con questi mattoni potete automatizzare la compilazione di qualunque progetto: regole, prerequisiti, variabili e wildcard. Provate a riscrivere i vostri progetti con un Makefile, vi cambierà la vita.

Link e risorse

Sostieni il canale

PER SOSTENERE IL CANALE BASTANO UN LIKE E UN’ISCRIZIONE!
ma se vuoi offrirmi un caffè, mi aiuterà a rimanere sveglio mentre registro i video 😁☕
clicca qui per scoprire come https://buymeacoffee.com/thelinuxnerd

questi link sono affiliati Amazon, acquistando i libri che ti consiglio sostieni il canale!