Archivi Categorie: C/C++

Il linguaggio di programmazione C/C++

QT – WEBINAR: Getting Started with Qt/C++ Programming. Parte 2

La scorsa volta ci siamo lasciati alla gestione degli eventi. Ecco cosa ci propone QT.

QT gestione eventi

Le QT usano gli SLOT e i SIGNAL per la comunicazione ad alto livello con l’utente (ad esempio il movimento del mouse o la pressione di un bottone…proprio quello che vogliamo noi :) ). I virtual method sono invece usati per interazioni a basso livello con il sistema operativo o ad esempio la pressione dei tasti… su una tastiera naturalmente.
Quindi il funzionamento di SLOT e SIGNAL è molto semplice. Un oggetto emette un segnale (il SIGNAL) e un altro risponde al segnale con una funzione particolare (lo SLOT).
La connessione tra i due viene fatta usando la funzione connect che appartiene alla classae QObject. I parametri della connect sono i seguenti (copiati dall’help QT):

bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoConnection ) [static]

Come farli funzionare nel nostro esempio della volta scorsa? Il sender e l’evento lo abbiamo, ci mancano il receiver e il metodo di risposta… almeno per il bottone “Ciao”. Perché per il bottone “Esci” useremo un artificio :)

Il codice è il seguente:

Eventibottoni.h

#ifndef EVENTIBOTTONI_H
#define EVENTIBOTTONI_H

#include <QObject>

class EventiBottoni : public QObject
{
    Q_OBJECT

public slots:
    void rispondiButton1();

};

#endif // EVENTIBOTTONI_H

Eventibottoni.cpp

#include <QMessageBox>

#include "eventibottoni.h"

void EventiBottoni::rispondiButton1()
{
    QMessageBox::information(0, QObject::tr("Messaggio"), QObject::tr("Ciao, mondo!!"));
}

main.cpp

#include <QApplication>
#include <QtGui>

#include "eventibottoni.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget window;

    QPushButton *button1 = new QPushButton("Ciao", &window);
    QPushButton *button2 = new QPushButton("Esci", &window);
    QHBoxLayout *layout = new QHBoxLayout(&window);

    layout->addWidget(button1);
    layout->addWidget(button2);

    EventiBottoni eventi;

    QObject::connect(button1, SIGNAL(clicked()), &eventi, SLOT(rispondiButton1()));
    QObject::connect(button2, SIGNAL(clicked()), qApp, SLOT(quit()));

    //window.setLayout(layout);
    window.show();

    return a.exec();
}

E questo è il risultato:

Pulsante ciao

Allora come si vede abbiamo dovuto generare un oggetto derivante da QObject in cui definire il metodo di risposta alla pressione del tasto “Ciao”. L’oggetto deve forzatamente derivare da QObject pena il non poter definire la connect. Se vi chiedete il perché guardate la definizione della funzione :) l’ho messa a posta all’inizio.

L’artificio per il bottone “Esci” invece è semplice :) ho definito una connessione verso l’oggetto qApp (che non abbiamo definito noi) ma è definito dalle QT ed è un puntatore globale all’oggetto applicazione. Per cui diciamo all’applicazione di chiudersi chiamando lo slot quit().

Ok Siamo quasi arrivati alla fine, il webminar era di soli 45 minuti, manca solo un punto da chiarire. Quel “Q_OBJECT” che vedete in mezzo alla classe abilita il MOC, cioè il Meta object compiler. Diciamo che è il traduttore di alcune feature QT verso codice standard C++ ad esempio converte le funzioni signals e slots in codice C++ al momento della compilazione. Un sunto lo trovate nell’immagine di seguito:

MOC

Il mio prossimo webinar sarà il 13 di marzo :) per cui potete stare tranquilli per un po’. :P

QT – WEBINAR: Getting Started with Qt/C++ Programming. Parte 1

Che dire… con le QT (si pronuncia cute) è stato amore a prima vista :) spero mia moglie non scopra con chi passo le notti :D Per cui dato che hanno risvegliato i miei “peggiori istinti Nerd” :) mi sono iscritto ad un paio di webminar. Se vi interessano i webinar li trovate qui mentre i link ai miei precedenti post sono questo e questo.
Vi riporto di seguito alcune info/tips che ho ascoltato nel webinar “Getting Started with Qt/C++ Programming”.

Il segreto della portabilità è qmake che genera il makefile adatto al sistema opertativo usato. Ottenuto il make file si potrà passare alla compilazione. Questo è il mio output di qmake in ambiente windows, notate il parametro “-spec win32-g++”:

14:22:22: Running steps for project DBTest...
14:22:22: Starting: "C:\Qt\Qt5.0.1\5.0.1\mingw47_32\bin\qmake.exe" C:\Users\wildwolf\Desktop\DBTest\DBTest\DBTest.pro -r -spec win32-g++ "CONFIG+=debug" "CONFIG

+=declarative_debug" "CONFIG+=qml_debug"
14:22:25: The process "C:\Qt\Qt5.0.1\5.0.1\mingw47_32\bin\qmake.exe" exited normally.

La compilazione invece è stata questa. Notate l’utilizzo di “mingw32-make.exe” che è il “Minimalist GNU for Windows” cioè il compilatore GNU minimale per winzoz.

14:24:10: Running steps for project DBTest...
14:24:11: Starting: "C:\Qt\Qt5.0.1\Tools\MinGW\bin\mingw32-make.exe" clean
C:/Qt/Qt5.0.1/Tools/MinGW/bin/mingw32-make -f Makefile.Debug clean
mingw32-make[1]: Entering directory 'C:/Users/wildwolf/Desktop/DBTest/DBTest-build-Desktop_Qt_5_0_1_MinGW_32bit-Debug'
rm -f debug/moc_mainwindow.cpp
rm -f ui_mainwindow.h
rm -f debug/main.o debug/mainwindow.o debug/moc_mainwindow.o
mingw32-make[1]: Leaving directory 'C:/Users/wildwolf/Desktop/DBTest/DBTest-build-Desktop_Qt_5_0_1_MinGW_32bit-Debug'
C:/Qt/Qt5.0.1/Tools/MinGW/bin/mingw32-make -f Makefile.Release clean
mingw32-make[1]: Entering directory 'C:/Users/wildwolf/Desktop/DBTest/DBTest-build-Desktop_Qt_5_0_1_MinGW_32bit-Debug'
rm -f release/moc_mainwindow.cpp
rm -f ui_mainwindow.h
rm -f release/main.o release/mainwindow.o release/moc_mainwindow.o
mingw32-make[1]: Leaving directory 'C:/Users/wildwolf/Desktop/DBTest/DBTest-build-Desktop_Qt_5_0_1_MinGW_32bit-Debug'
14:24:37: The process "C:\Qt\Qt5.0.1\Tools\MinGW\bin\mingw32-make.exe" exited normally.
14:24:37: Configuration unchanged, skipping qmake step.
14:24:37: Starting: "C:\Qt\Qt5.0.1\Tools\MinGW\bin\mingw32-make.exe" 
C:/Qt/Qt5.0.1/Tools/MinGW/bin/mingw32-make -f Makefile.Debug
mingw32-make[1]: Entering directory 'C:/Users/wildwolf/Desktop/DBTest/DBTest-build-Desktop_Qt_5_0_1_MinGW_32bit-Debug'
C:/Qt/Qt5.0.1/5.0.1/mingw47_32/bin/uic.exe ../DBTest/mainwindow.ui -o ui_mainwindow.h
g++ -c -pipe -fno-keep-inline-dllexport -g -frtti -Wall -Wextra -fexceptions -mthreads -DUNICODE -DQT_QML_DEBUG -DQT_DECLARATIVE_DEBUG -DQT_WIDGETS_LIB -DQT_SQL_LIB -
DQT_GUI_LIB -DQT_CORE_LIB -DQT_OPENGL_ES_2 -DQT_OPENGL_ES_2_ANGLE -DQT_NEEDS_QMAIN -I../DBTest -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtWidgets' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtSql' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtGui' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtCore' -I'debug' -I'.' -I'.' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/mkspecs/win32-g++' -o debug/main.o ../DBTest/main.cpp
g++ -c -pipe -fno-keep-inline-dllexport -g -frtti -Wall -Wextra -fexceptions -mthreads -DUNICODE -DQT_QML_DEBUG -DQT_DECLARATIVE_DEBUG -DQT_WIDGETS_LIB -DQT_SQL_LIB -
DQT_GUI_LIB -DQT_CORE_LIB -DQT_OPENGL_ES_2 -DQT_OPENGL_ES_2_ANGLE -DQT_NEEDS_QMAIN -I../DBTest -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtWidgets' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtSql' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtGui' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtCore' -I'debug' -I'.' -I'.' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/mkspecs/win32-g++' -o debug/mainwindow.o ../DBTest/mainwindow.cpp
C:/Qt/Qt5.0.1/5.0.1/mingw47_32/bin/moc.exe -DUNICODE -DQT_QML_DEBUG -DQT_DECLARATIVE_DEBUG -DQT_WIDGETS_LIB -DQT_SQL_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_OPENGL_ES_2 -
DQT_OPENGL_ES_2_ANGLE -DQT_NEEDS_QMAIN -I../DBTest -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtWidgets' 
-I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtSql' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtGui' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtCore' -I'debug' -I'.' -I'.' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/mkspecs/win32-g++' -D__GNUC__ -DWIN32 
../DBTest/mainwindow.h -o debug/moc_mainwindow.cpp
g++ -c -pipe -fno-keep-inline-dllexport -g -frtti -Wall -Wextra -fexceptions -mthreads -DUNICODE -DQT_QML_DEBUG -DQT_DECLARATIVE_DEBUG -DQT_WIDGETS_LIB -DQT_SQL_LIB -
DQT_GUI_LIB -DQT_CORE_LIB -DQT_OPENGL_ES_2 -DQT_OPENGL_ES_2_ANGLE -DQT_NEEDS_QMAIN -I../DBTest -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtWidgets' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtSql' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtGui' -I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/include/QtCore' -I'debug' -I'.' -I'.' -
I'../../../../../Qt/Qt5.0.1/5.0.1/mingw47_32/mkspecs/win32-g++' -o debug/moc_mainwindow.o debug/moc_mainwindow.cpp
g++ -Wl,-subsystem,windows -mthreads -o debug/DBTest.exe debug/main.o debug/mainwindow.o debug/moc_mainwindow.o  -lmingw32 -lqtmaind -LC:\Qt\Qt5.0.1\5.0.1\mingw47_32\lib 
-lQt5Widgetsd -lQt5Sqld -lQt5Guid -lQt5Cored -llibEGLd -llibGLESv2d -lgdi32 -luser32 
mingw32-make[1]: Leaving directory 'C:/Users/wildwolf/Desktop/DBTest/DBTest-build-Desktop_Qt_5_0_1_MinGW_32bit-Debug'
14:25:07: The process "C:\Qt\Qt5.0.1\Tools\MinGW\bin\mingw32-make.exe" exited normally.

Ed in ambiente GNU Linux? :) Tutto uguale a parte che qmake imposta il make file per linux e quindi trovate il parametro “-spec linux-g++”

14:43:38: Running steps for project DBTest...
14:43:38: Starting: "/usr/bin/qmake" /home/wildwolf/Documenti/Projects/CPP/DBTest/DBTest/DBTest.pro -r -spec linux-g++ CONFIG+=debug CONFIG+=declarative_debug
14:43:40: The process "/usr/bin/qmake" exited normally.

Non vi riporto la sbrodolata della compilazione :) … e non cercate di farmi credere che qualcuno ha letto quella di prima :P
Ok andiamo oltre la compilazione… Per chi soffre di insonnia da memory leak e sindrome da delete, c’è una bella notizia. Quando si alloca un puntatore di un oggetto QT e questo ha un padre, cioè, vi riporto le parole del docente del webinar, “nel costruttore dell’oggetto viene passato come ultimo parametro un oggetto padre” allora questo si occuperà di deallocare la memoria, se poi noi siamo presi da forme di “delete” compulsivo, un oggetto figlio comunicherà comunque al padre la sua deallocazione e quindi non verrà eliminato 2 volte :) come dire… “quasi salvi” :P

Quindi il codice del nostro applicativo base con 2 bottoni sarà:

#include <QApplication>
#include <QtGui>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget window;

    QPushButton *button1 = new QPushButton("Ciao", &window);
    QPushButton *button2 = new QPushButton("Esci",&window);
    QHBoxLayout *layout = new QHBoxLayout();

    layout->addWidget(button1);
    layout->addWidget(button2);

    window.setLayout(layout);
    window.show();

    return a.exec();
}

Il nostro mini applicativo… va beh finestra con 2 bottoni :) farà quindi vedere 2 bottoni :) come in figura:

Applicazione

e dato che abbiamo applicato un layout orizzontale, allargando la finestra otterremo un risultato simile:

Applicazione strechata

button1 e button2 avendo definito un parent saranno deallocati in automatico e il layout verrà sempre eliminato da window perché impostato con la setLayout. E window? Beh è allocato sullo stack :) per cui alla chiusura del programma ci saluterà senza recriminare alcunchè :)

Avremmo potuto scrivere il codice anche così, assegnando un parent al nostro layout.

#include <QApplication>
#include <QtGui>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget window;

    QPushButton *button1 = new QPushButton("Ciao", &window);
    QPushButton *button2 = new QPushButton("Esci", &window);
    QHBoxLayout *layout = new QHBoxLayout(&window);

    layout->addWidget(button1);
    layout->addWidget(button2);

    //window.setLayout(layout);
    window.show();

    return a.exec();
}

Per adesso è tutto. La prossima volta faremo fare qualche cosa con i 2 bottoni. Per concludere una notiziona… le QT 5.2 dovrebbero permettere di creare applicativi Android, si troverà una beta delle API con la versione 5.1

Ancora QT e ancora database.

Comincio subito con il ringraziare b3h3m0th che mi ha segnalato che le QT non sono più di Nokia dalla scorsa estate e adesso appartengono a digia.

Dopo essere rimasto favorevolmente impressionato dalle QT ho deciso di approfondirle ulteriormente andando a complicare l’esempio di collegamento al database. L’idea era di avere una griglia con l’elenco dei record e un dettaglio separato (in modo da sincronizzare il record attivo sulla griglia con una parte separata ma dipendente)
La finestra ha l’aspetto in figura.

Il nostro primo risultato

Lo schema del database era il seguente:

CREATE TABLE tblProvincia
(cod character varying(2) NOT NULL,
 des character varying(40)
);

CREATE TABLE tbllocalita
(
  cod character varying(4) NOT NULL,
  des character varying(40),
  fk_provincia_cod character varying(2)
);

Il codice C++ invece è il seguente:

#include <QMessageBox>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>
#include <QTableWidgetItem>
#include <QDir>
#include <QSqlQuery>
#include <QtSql>
#include <QDataWidgetMapper>

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
  ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    db.close();
    delete ui;
}

void MainWindow::on_btnDBConnection_clicked()
{
    //Connessione al DB
    db = QSqlDatabase::addDatabase("QSQLITE");

    QString path(QDir::home().path());
    path.append(QDir::separator()).append("qtTest.sqlite");
    path = QDir::toNativeSeparators(path);
    db.setDatabaseName(path);
    bool ok = db.QSqlDatabase::open();
    if (!ok)
        QMessageBox::critical(0, QObject::tr("Errore database"), db.lastError().text());

}

void MainWindow::on_btnQuery_clicked()
{

    if (db.isOpen())
    {
        QSqlRelationalTableModel *model = new QSqlRelationalTableModel;

        model->setTable("tbllocalita");
        provIndex = model->fieldIndex("fk_provincia_cod");
        model->setRelation(provIndex, QSqlRelation("tblprovincia", "cod", "des"));
        model->setHeaderData(0, Qt::Horizontal, tr("Codice"));
        model->setHeaderData(1, Qt::Horizontal, tr("Descrizione"));
        model->setHeaderData(2, Qt::Horizontal, tr("Provincia"));
        model->select();

        ui->tblData->setModel(model);

        QSqlTableModel *relModel = model->relationModel(provIndex);
        ui->qcbProvincia->setModel(relModel);
        ui->qcbProvincia->setModelColumn(relModel->fieldIndex("des"));

        QDataWidgetMapper *mapper = new QDataWidgetMapper;
        mapper->setModel(model);
        mapper->setItemDelegate(new QSqlRelationalDelegate(this));
        mapper->addMapping(ui->pteCod, 0);
        mapper->addMapping(ui->pteDes, 1);
        mapper->addMapping(ui->qcbProvincia, provIndex);

        mapper->toFirst();
        connect(ui->tblData->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), mapper, SLOT(setCurrentModelIndex(QModelIndex)));

    }
}

void MainWindow::on_btnExit_clicked()
{
    QApplication::quit();
}

Rispetto al mio precedente esempio ci sono poche modifiche. La prima è che ho dovuto usare un modello dati di tipo QSqlRelationalTableModel. In questo modo ho potuto impostare una relazione tra il campo fk_provincia_cod della tabella tbllocalita e il codice della tblprovincia in modo da salvare il codice come dato ma vedere la sua descrizione come visualizzazione. In parole povere la classica normalizzazione della base dati.
Usando un modello di tipo QSqlRelationalTableModel ho dovuto eliminare la query ed impostare una tabella di partenza:

model->setTable("tbllocalita");

ed impostarne la relazione con la tblprovincia:

provIndex = model->fieldIndex("fk_provincia_cod");
model->setRelation(provIndex, QSqlRelation("tblprovincia", "cod", "des"));

Attenzione… se siete tentati come me nella prima versione a non dichiarare una variabile provIndex di tipo int e ad usare ovunque direttamente

model->fieldIndex("fk_provincia_cod")

non fatelo… otterreste un bruttissimo errore di “QComboBox::setModel: cannot set a 0 model” a runtime alla riga:

ui->qcbProvincia->setModelColumn(relModel->fieldIndex("des"));

Sinceramente non capisco il perché dell’errore… se qualcuno ha dei suggerimenti e ben accetto :)

Dopo di che ho usato un QDataWidgetMapper per legare i widget alla sorgente dati:

QDataWidgetMapper *mapper = new QDataWidgetMapper;
mapper->setModel(model);
mapper->setItemDelegate(new QSqlRelationalDelegate(this));
mapper->addMapping(ui->pteCod, 0);
mapper->addMapping(ui->pteDes, 1);
mapper->addMapping(ui->qcbProvincia, provIndex);

E per ultimo per far si che allo spostarsi della cella attiva sulla griglia venissero aggiornati i dati dei widget sotto la stessa ho creato una connessione tra il segnale dell’oggetto mittente (sender) e il metodo dell’oggetto ricevente (receiver).

connect(ui->tblData->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), mapper, SLOT(setCurrentModelIndex(QModelIndex)));

Come si vede nella finestra anche la combo è popolata con le descrizioni giuste.

Una bella combo box con i miei dati

Le istruzioni per caricare i dati nella combo sono state le seguenti:

ui->qcbProvincia->setModel(relModel);
ui->qcbProvincia->setModelColumn(relModel->fieldIndex("des"));
...
...
mapper->addMapping(ui->qcbProvincia, provIndex);

E se si volesse fare un collegamento master detail? Altra prassi comune in applicativi database oriented… le cose :) rimangono ancora semplici.

Master - Detail

Le tabelle usate hanno la seguente struttura:

CREATE TABLE tblPersona (
  ID INTEGER,
  NOME character varying(40),
  PRIMARY KEY (ID)
);

CREATE TABLE tblFilm (
  ID INTEGER,
  FK_PERSONA_ID INTEGER,
  TITOLO character varying(40),
  PRIMARY KEY (ID),
  FOREIGN KEY (FK_PERSONA_ID) REFERENCES tblPersona
);

Il codice C++ è invece il seguente:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QtSql/QSqlDatabase>
#include <QSqlRelationalTableModel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_btnDBConnection_clicked();
    void on_btnExit_clicked();
    void updateDetailGrid();
    void on_btnOpenTable_clicked();

private:
    Ui::MainWindow *ui;

    QSqlDatabase db;
    QSqlRelationalTableModel *masterModel;
    QSqlRelationalTableModel *detailModel;

};

e poi,

#include <QDir>
#include <QMessageBox>
#include <QSqlError>
#include <QSqlRelationalTableModel>
#include <QSqlRecord>

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    masterModel = NULL;
    detailModel = NULL;

    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    db.close();
    if (masterModel == NULL)
        delete masterModel;
    if (detailModel == NULL)
        delete detailModel;

    delete ui;
}

void MainWindow::on_btnDBConnection_clicked()
{
    //Connessione al DB
    db = QSqlDatabase::addDatabase("QSQLITE");

    QString path(QDir::home().path());
    path.append(QDir::separator()).append("qtTest.sqlite");
    path = QDir::toNativeSeparators(path);
    db.setDatabaseName(path);
    bool ok = db.QSqlDatabase::open();
    if (!ok)
        QMessageBox::critical(0, QObject::tr("Errore database"), db.lastError().text());

}

void MainWindow::updateDetailGrid()
{
    QModelIndex index = ui->vieGridMaster->currentIndex();
    if (index.isValid())
    {
        QSqlRecord record = masterModel->record(index.row());
        int id = record.value("id").toInt();
        detailModel->setFilter(QString("fk_persona_id = %1").arg(id));
    }
    else
        detailModel->setFilter("fk_persona_id = -1");

    detailModel->select();
    ui->vieGridDetail->horizontalHeader()->setVisible(detailModel->rowCount() > 0);
}

void MainWindow::on_btnExit_clicked()
{
    QApplication::quit();
}

void MainWindow::on_btnOpenTable_clicked()
{
    if (db.isOpen())
    {
        masterModel = new QSqlRelationalTableModel;
        masterModel->setTable("tblpersona");
        masterModel->setHeaderData(0, Qt::Horizontal, tr("ID"));
        masterModel->setHeaderData(1, Qt::Horizontal, tr("Nome"));
        masterModel->select();

        detailModel = new QSqlRelationalTableModel;
        detailModel->setTable("tblfilm");
        detailModel->setHeaderData(0, Qt::Horizontal, tr("ID"));
        detailModel->setHeaderData(1, Qt::Horizontal, tr("fk_Perosna_id"));
        detailModel->setHeaderData(2, Qt::Horizontal, tr("Titolo"));
        detailModel->select();

        ui->vieGridMaster->setModel(masterModel);
        ui->vieGridDetail->setModel(detailModel);

        connect(ui->vieGridMaster->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), this, SLOT(updateDetailGrid()));
        ui->vieGridMaster->selectRow(0);
    }
}

Si creano 2 datamodel di tipo QSqlRelationalTableModel. Dopo di che si crea un evento che collega il cambio di riga selezionata sulla griglia master e lo comunica alla funzione che si occuperà di aggiornare il dettaglio, la funzione updateDetailGrid. Quest’ultima per aggiornare i propri dati crea un filtro sul suo modello dati e aggiorna l’estrazione. Veramente facile :) dopo che si ha acquisito un po’ di esperienza.

Per concludere il mio parere personale è che QT sia un’ottima libreria per lo sviluppo di applicativi client multipiattaforma (insomma mi ha dato una bella soddisfazione usarla per questi test :) ).E’ sicuramente ben studiata e soprattutto va studiata… e con l’avvento di internet (e Google) almeno io tendo a cercare la soluzione piuttosto che leggermi i manuali, per cui l’occhio sui QDataWidgetMapper mi è caduto dopo vari tentativi e ore perse a leggermi vari post. La parte di accesso all’SQL sembra meno curata del resto (ma è la mia prima impressione da utilizzatore di altri framework). L’utilizzo di un filtro per il master/detail funziona bene con pochi record… ma se questi aumentassero? Sicuramente ci sono altri framwork di sviluppo (ad esempio Delphi e la sua VCL) che sono più flessibili e sofisticati (anche nell’integrazione con l’IDE), ma evidentemente gli sviluppatori QT hanno deciso di porre l’enfasi su altri aspetti del framework e in tutta franchezza CHE ASPETTI :)

Sviluppo multipiattaforma con QT

A settembre 2011 Embarcadero annunciava il suo Delphi XE2 con la libreria firemonkey che permettevano una compilazione di un eseguibile tra piattaforme diverse. Un solo sorgente per Windows e OSX.
In realtà esiste già da tempo una libreria che permette di fare compilazioni cross-platform e si chiama QT un solo sorgente per i sitemi Linux/OSX/Windows e inoltre embedded Linux e Symbian. Così giusto per fare una qualche prova e scrivere un po’ di codice in C++ ho provato a fare un programma che si collegasse ad un database sqlite :)
Con le librerie Qt viene anche fornito un IDE, QT Creator, che permette di sviluppare i propri applicativi. Esiste una versione opensource per i progetti non commerciali altrimenti bisogna investire qualche euro (per sapere i prezzi bisogna contattare il loro team vendite).
Per la creazione della for ho usato il designer visuale integrato con il QT Creator. 3 bottoni, connessione al DB, esecuzione query e uscita dall’applicativo e una griglia per visualizzare i dati. La griglia è una QTableWidget (non ‘è l’oggetto consigliato per i dati che arrivano da un database, ma per la prova va benissimo).

Per usare le librerie per i database ho dovuto aggiungere sql nel file di progetto “.pro” Che di default aveva questo contenuto:

#-------------------------------------------------
#
# Project created by QtCreator 2013-02-07T11:50:15
#
#-------------------------------------------------

QT += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = DBTest
TEMPLATE = app

SOURCES += main.cpp\
 mainwindow.cpp

HEADERS += mainwindow.h

FORMS += mainwindow.ui

Che io ho cambiato in:

QT += core gui sql

Il file header (mainwindow.h) è il seguente:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QtSql/QSqlDatabase>

namespace Ui {
  class MainWindow;
}

class MainWindow : public QMainWindow
{
 Q_OBJECT

  public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

  private slots:
    void on_btnDBConnection_clicked();
    void on_btnQuery_clicked();

  private:
    Ui::MainWindow *ui;
    QSqlDatabase db;
};

#endif // MAINWINDOW_H

Il file sorgente è invece questo.

#include <QMessageBox>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>
#include <QTableWidgetItem>
#include <QDir>
#include <QSqlQuery>
#include <QtSql>

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
 QMainWindow(parent),
 ui(new Ui::MainWindow)
{
 ui->setupUi(this);
}

MainWindow::~MainWindow()
{
 db.close();
 delete ui;
}

void MainWindow::on_btnDBConnection_clicked()
{
  //Connessione al DB
  db = QSqlDatabase::addDatabase("QSQLITE");
  QString path(QDir::home().path());
  path.append(QDir::separator()).append("qtTest.sqlite");
  path = QDir::toNativeSeparators(path);
  db.setDatabaseName(path);
  bool ok = db.QSqlDatabase::open();
  if (!ok)
    QMessageBox::critical(0, QObject::tr("Errore database"), db.lastError().text());
}

void MainWindow::on_btnQuery_clicked()
{
  if (db.isOpen())
  {
    QSqlQuery query("Select * from tbllocalita");

    int codNo = query.record().indexOf("cod");
    int desNo = query.record().indexOf("des");
    ui->tblData->insertColumn(0);
    ui->tblData->insertColumn(1);
    while (query.next())
    {
      QString sCod = query.value(codNo).toString();
      QString sDes = query.value(desNo).toString();
      int row = ui->tblData->rowCount();
      ui->tblData->insertRow(row);
      ui->tblData->setItem(row, 0, new QTableWidgetItem(sCod));
      ui->tblData->setItem(row, 1, new QTableWidgetItem(sDes));
    }
  }
}

E’ da notare la definizione del tipo di database a cui connettersi:

db = QSqlDatabase::addDatabase("QSQLITE");

I database supportati sono i seguenti (copio e incollo dal sito):

“IBM DB2 (version 7.1 and above)Borland InterBase
MySQL
Oracle Call Interface Driver
Open Database Connectivity (ODBC) – Microsoft SQL Server and other ODBC-compliant databases
PostgreSQL (versions 7.3 and above)
SQLite version 2
SQLite version 3
SQLite version 3 for Symbian SQL Database
Sybase Adaptive Server Note: obsolete from Qt 4.7″

Trovate più informazioni sui database qui.

Dopo la scelta del tipo di database, bisognerà definire i parametri di connessione. Nel caso in questione avendo scelto sqlite si dovrà definire la path del file (l’ultima riga), ed ecco comparire alcune utili funzioni integrate in QT che permettono di otterene la path della propria home e di riscrivere i separatori delle cartelle rispettando lo standard del sistema operativo su cui gira il programma (le prime 3 righe).

QString path(QDir::home().path());
path.append(QDir::separator()).append("qtTest.sqlite");
path = QDir::toNativeSeparators(path);
db.setDatabaseName(path);

Di seguito potete vedere come nella finestra di debug le path siano corrette:

Path in ambiente windows

Path in ambiente windows

Path in ambiente Linux

Path in ambiente Linux

Una volta aperto il DB, potremo fare la nostra estrazione dei dati:

if (db.isOpen())
{
  QSqlQuery query("Select * from tbllocalita");

  int codNo = query.record().indexOf("cod");
  int desNo = query.record().indexOf("des");
  ui->tblData->insertColumn(0);
  ui->tblData->insertColumn(1);
  while (query.next())
  {
    QString sCod = query.value(codNo).toString();
    QString sDes = query.value(desNo).toString();
    int row = ui->tblData->rowCount();
    ui->tblData->insertRow(row);
    ui->tblData->setItem(row, 0, new QTableWidgetItem(sCod));
    ui->tblData->setItem(row, 1, new QTableWidgetItem(sDes));
  }
}

Sempre in questo esempio la tabella era banale e aveva solo 2 colonne, ma nel caso ve ne siano molte di più si può vedere che è possibile abbinare un identificativo menmonico (una variabile di tipo intero) alla colonna fisica della query risultante in maniera da poterla usare per ottenere il valore del campo. Si è vero si poteva scrivere anche così:

QString sCod = query.value(query.record().indexOf("cod")).toString();
QString sDes = query.value(query.record().indexOf("des")).toString();

Infine i dati vengono salvati nella griglia, oggetto tbldata.

int row = ui->tblData->rowCount();
ui->tblData->insertRow(row);
ui->tblData->setItem(row, 0, new QTableWidgetItem(sCod));
ui->tblData->setItem(row, 1, new QTableWidgetItem(sDes));

La funzione insertRow inserisce una riga vuota alla posizione specificata da row, per cui nel nostro caso ci viene comodo impostarla sempre al numero di righe inserite che partono da 0.

Si compila quindi il tutto e lo stesso sorgente genera due eseguibili diversi funzionanti nello stesso modo su Windows e Linux.

Binario Linux

Binario Linux

Binario Windows

Binario Windows

Si lo so… i più bravi saranno già li a dire che con QT esisteno le QTableView a cui è possibile abbinare direttamente un modello dati di tipo QSqlQueryModel (che è in sola lettura) o QSqlTableModel (anche in scrittura). Per cui il codice in on_btnQuery_clicked diventerà molto semplicemente:

QSqlQueryModel *model = new QSqlQueryModel;
QSqlQuery query("Select cod, des from tbllocalita");

model->setQuery(query);
model->setHeaderData(0, Qt::Horizontal, tr("Codice"));
model->setHeaderData(1, Qt::Horizontal, tr("Descrizione"));

ui->tblData->setModel(model);

Speriamo che Nokia non decida in futuro di farci pagare questo gioiellino :)

Comportamenti indefiniti

ResearchBlogging.org

Il quiz dello scorso post era un pochino a trabocchetto, lo ammetto, ma Alessandro ha fatto un buon lavoro ed è quasi arrivato alla soluzione. Il quiz era il seguente:

Qual’è il comportamento del seguente programmino C?

#include <stdio.h>

int d = 5;

int set(int x)
{
    return d = x;
}

int main()
{
    int r = set(0) + 7 / d;
    printf("risultato = %d\n", r);
    return 1; 
}

E la risposta è

(rullo di tamburi)

il programma ha un comportamento indefinito!

Come, non sapete cos’è un comportamento indefinito? Beh, se programmate in C, è il momento di fare un corso di aggiornamento.

Benvenuti nel fantasmagorico mondo degli Undefined Behaviours!

Se volete una introduzione veloce, al solito c’è wikipedia. Se non ci avete capito molto, vi consiglio questa serie di tre post del bravissimo John Regher (tra l’altro, John ha anche un corso su Udacity, sul testing: se avete tempo di seguirvi un intero corso e volete studiare software testing, il corso di John è quello che fa per voi!).

Se invece volete continuare a leggere, vi spiego in quattro e quattr’otto di cosa stiamo parlando. La semantica del C/C++ è un po lasca, e in certi punti lascia libertà al compilatore. L’esempio classico è quello dell’accesso a un array al di là della sua dimensione. Per esempio, se avete un array di 10 interi, e accedete all’11-esima posizione, che succede? Lo standard C vi dice che, in tal caso, il comportamento del programma è “undefined”.

Che deve fare un compilatore quando si trova di fronte a un comportamento indefinito? Beh, l’interpretazione più diffusa è che il compilatore è libero di fare quello che gli pare. Qualunque cosa, e per “qualunque cosa” intendo proprio qualsiasi. Infatti, un programma che ha un “undefined behaviour” è semanticamente incorretto, e quindi da una cosa scorretta si può tirar fuori qualunque altra cosa, no? Persino dei diavoletti dal naso (nasal demons!).

Torniamo al quiz e vediamo perché il programma ha un comportamento indefinito. In C, l’ordine con cui vengono valutati gli operandi di una espressione non è sempre ben definito. Ci sono regole un pochino complicate, che hanno a che fare con i cosidetti sequence points. In pratica, un sequence point è un punto del programma in cui, prima di proseguire bisogna acertarsi di aver risolto tutti i “side effects” delle istruzioni precedenti. Il problema del nostro programma è che nella fattispecie l’operatore di somma non è un sequence point. Quando scriverte (x + y), il compilatore è libero di calcolare prima x e poi y, oppure viceversa. Di solito, l’ordine di valutazione degli argomenti non è importante: dopo tutto, in matematica l’addizione gode della proprietà commutativa, quindi ci si aspetta che cambiando l’ordine degli addendi il risultato non cambi! Per cui, chi ha scritto lo standard ha lasciato libero il compilatore di eseguire la valutazione degli addendi nell’ordine che preferiva. Per esempio, se per ragioni di ottimizzazione, è meglio valutare prima y e poi x, il compilatore lo farà, perché è libero di farlo, secondo lo standard.

Purtroppo, nel nostro programma, valutare prima la chiamata set(0) e poi 7/d, oppure prima 7/d e poi set(0) cambia il risultato: nel primo caso si ha una bella divisione per 0; nel secondo caso invece il risultato dovrebbe essere 1.
Poiché il risultato non è univoco, ma dipende dall’ordine in cui il compilatore decide di valutare i due operandi, allora il nostro programma ha un “Undefined Behaviour”. In pratica, è scorretto e non aderente allo standard. Tenete anche presente che una cosa è la precedenza fra operatori, e una cosa completamente diversa è l’ordine di valutazione degli operandi: la differenza è spiegata piuttosto bene qui.

Nella pratica, in questo caso il compilatore gcc segue sempre l’ordine di valutazione da sinistra verso destra, per cui se compilate ed eseguite il programma con gcc avrete come risultato un bell’errore di divisione per zero.

Più interessante è il comportamento di un altro compilatore che ultimamente sembra andare per la maggiore: si tratta di clang. Se compilate con il comando:

$ clang prova.c
$ ./a.out
Floating point exception (core dumped)

che è quello che ci si aspetta. Mentre se compilate con le ottimizzazioni:

$ clang -O2 prova.c
$ ./a.out
risultato = 894604808

Il numero che tira fuori è assolutamente casuale! Come mai? Beh, semplice: come detto prima, in presenza di “Undefined Behaviour” tutto può succedere, anche che vi escano i demoni dal naso! Più seriamente: Se il programma è non corretto (ha dei comportamenti indefiniti), il compilatore può tirare fuori eseguibili non corretti, non necessariamente nel modo che ci si aspetta.

Soluzioni?

Adesso la parte seria. Recentemente due ricercatori dell’University of Illinois at Urbana-Champaign (UIUC), Ellison e Rosu, hanno scritto un articolo molto interessante in cui si sono presi la briga di specificare in maniera formale la semantica del linguaggio C, utilizzando un altro linguaggio formale chiamato K. A differenza di altre specifiche formali, quella proposta da Ellison e Rosu è eseguibile, e questo significa che possono prendere un programma C e analizzarlo formalmente, ad esempio alla ricerca di undefined behaviours. Il loro codice è disponibile su googlecode, e teoricamente avrei potuto provarlo sul nostro programmino di esempio. (Ci ho provato per un po’ ma purtroppo il processo di compilazione mi si ferma a metà, devo aver sbagliato qualcosa. Se ci riesco ve lo faccio sapere.) Comunque, il programma dei nostri due ricercatori dovrebbe identificare la presenza di un undefined behaviour nel nostro programmino di quiz e metterlo bene in evidenza.

Il lavoro di Ellison e Rosu è molto importante: dato che molto codice per sistemi embedded e sistemi critici è ancora scritto in C, spesso in maniera euristica o artigianale, avere a disposizione strumenti automatici di verifica è essenziale per l’eliminazione di errori che potrebbero addirittura mettere a rischio la nostra sicurezza. La mia previsione è che nel futuro sentiremo parlare sempre di più di strumenti di verifica formale come assistenza alla programmazione.

Chucky Ellison, Grigore Rosu (2012). An executable formal semantics of C with applications ACM SIGPLAN Notices, 47 (1), 533-544 : 10.1145/2103621.2103719

Ottimizzare il codice

We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.

Questa frase è stata attribuita a Donald E. Knuth, e se lo dice lui che ha scritto “The Art of Computer Programming”, ha inventato il TeX, e ha sviluppato la teoria della complessità degli algoritmi, potete stare certi che sia vero.

Il significato della frase è che bisognerebbe evitare di ottimizzare il codice prima di aver finito di scriverlo. Il modo di programmare “corretto” sarebbe:

  • primo, si scrive il codice nella maniera più semplice possibile, possibilmente in maniera elegante e leggibile
  • poi ci si accerta della sua correttezza (testing , debugging, e quello che pare a voi)
  • Infine, pensiamo alle performance, ottimizzando il codice dove necessario

Spesso invece il programmatore comincia fin dall’inizio a complicare il codice e la strrutra del programma, alla ricerca del programma già perfettamente ottimizzato. Nel fare così, spesso viene fuori codice molto complicato e probabilmente pieno di bug. Da qui la sentenza di Knuth: l’ottimizzazione prematura è la radice di tutti i mali!

L’osservazione chiave è che spesso il codice contiene alcuni (pochi) “colli di bottiglia”, ed è lì che l’ottimizzatore deve concentrarsi per far andare veloce il suo programma. Come identificare questi “colli di bottiglia”? Per esempio, funzioni chiamate molto frequentemente, strutture dati usate pesantemente in tutto il codice, ecc. I programmatori esperti ormai lo fanno ad occhio, ma per programmi molto grandi e complessi non è affatto facile. Sarebbe bene ci fosse qualche strumento di ausilio al programmatore.

oggi vediamo come fare analisi delle prestazioni con due tool disponibili su Linux con licenza GPL: valgrind e kcachegrind.

Il primo è una specie di “emulatore” del vostro PC. Prende in ingresso un file eseguibile, e lo esegue su un “processore virtuale”. Nel portare avanti l’esecuzione, però, è in grado di effettuare una serie di analisi sul codice. Valgrind è una piattaforma generica su cui sono stati sviluppati diversi strumenti. Il più famoso è memchek che serve a controllare l’uso della memoria, ed individuare possibili “memory leak“. Quello di cui ci occupiamo oggi, invece, è callgrind che serve a stimare le performance delle varie parti di codice del nostro programma.

Per installare questi tool, su Ubuntu si deve scrivere sul terminale:

sudo apt-get install valgrind
sudo apt-get install kcachegrind

e siamo pronti per i nostri esperimenti.

Proveremo ad utilizzarlo sui programmini che Juhan ci ha proposto per fare confronti fra il Fortran e vari altri linguaggi. Cominciamo dalla versione C/C++ di tale programma, che riporto qui per comodità (anche perché ho fatto due modifiche).

#include <stdio.h>
#include <math.h>

double f[14];

#define POW(x,y) pow(x,y)
//#define POW(x,y) mypow(x,y)

inline double mypow(double x, int y)
{
    double r = 1;
    for (int i=0; i<y; i++) r *= x;
    return r;
}

double fsin(double a) {
    return a - POW(a, 3) / f[3] + POW(a, 5) / f[5]
        - POW(a, 7) / f[7] + POW(a, 9) / f[9]
        - POW(a, 11) / f[11] + POW(a, 13) / f[13];
}

static double fcos(double a) {
    return 1.0 - POW(a, 2) / f[2] + POW(a, 4) / f[4]
        - POW(a, 6) / f[6] + POW(a, 8) / f[8]
        - POW(a, 10) / f[10] + POW(a, 12) / f[12];
}

static double myln(double x) {
    if (x == 0.0) {
        return -1.0e20;
    } else {
        double r = (x - 1.0) / (x + 1.0);
        return 2.0 * (r + POW(r, 3) / 3.0
                      + POW(r, 5) / 5.0
                      + POW(r, 7) / 7.0
                      + POW(r, 9) / 9.0);
    }
}

double mylog(double x) {
    static double ln10 = myln(10.0);

    return x / ln10;
}

int main(int argc, char **argv)
{

    int i, j;
    f[0] = 1.0;
    for (i = 1; i < 14; i++)
        f[i] = i * f[i - 1];
    int deg = 60;// * 60;
    int nsteps = 180 * deg;
    double step = M_PI / nsteps;
    double ssum;
    double csum;
    double a, s, c, t = 0.0;
    double ls, lc;
    for (j = 1; j < 11; j++)
    {
        ssum = 0.0;
        csum = 0.0;
        for (i = 0; i < nsteps; i++)
        {
            a = i * step;
            s = fsin(a);
            ls = mylog(myln(s));
            ssum += s;
            c = fcos(a);
            lc = mylog(myln(c));
            csum += c;
            if ((i % (10 * deg)) == 0)
            {
                if (c != 0.0)
                    t = s / c;
                printf("%3d %11.8f %11.8f %11.8f %15.8e\n", (i / deg), a, s, c, t);
                printf(" %15.8e %15.8e\n", ls, lc);
            }
        }
    }
    printf("%g\n", ssum);
    printf("%g\n", csum);
}

Le due modifiche fatte sono le seguenti: ho sostituito tutte le chiamate a pow() con la macro POW. Quest’ultima si traduce per il momento nella chiamata di libreria pow() (che, come discusso nel post precedente, è più lenta). Inoltre, ho diminuito il numero di cicli da svolgere per accorciare un po la durata dell’esecuzione: adesso il conto viene fatto per deg = 60 invece che per deg = 60*60 (vedi linea 54).

Salvate il programma qui su sul vostro HD con il nome prova.cpp, e poi compilatelo con il seguente comando:

g++ -g prova.cpp -o provacpp

Il flag -g serve per inserire i simboli di debug nel file eseguibile, e la qual cosa ci permetterà di associare il costo delle operazioni alle relative istruzioni nel codice sorgente.

Adesso siamo pronti per l’esecuzione, ecco il comando da dare:

valgrind --tool=callgrind ./provacpp

Questo manda in esecuzione il tool callgrind sul file eseguibile provacpp. Il quale viene eseguito a una velocità molto inferiore a quella normale (parliamo di un rallentamento di quasi 100 volte!) e vengono collezionati i dati di performance. L’output è un file che si chiama

callgrind.out.PID

Dove PID è l’ID del processo che è stato eseguito.
Per visualizzare i dati, date il comando:

kcachegrind callgrind.out.PID

(dove naturalmente avrete sostituito PID con il numero giusto), e si aprirà una finestra come questa:

La finestra principale di KCacheGrind

Questo tool ci visualizza i risultati dell’esecuzione. Faccio zoom sulla finesta a sinistra dove viene mostrato il “call graph” (ovvero il grafo delle chiamate):

Come vedete, il main() chiama le funzioni myln, fsin e fcos, e ciascuna chiama la funzione pow, la quale a sua volta chiama le funzioni interne per fare operazioni floating point. Subito sotto il nome delle funzioni viene riportato il tempo (in percentuale) speso dal programma dentro quella funzione. Questo programma ha speso il 95.73% del suo tempo dentro la funzione pow()! Notare anche una certa asimmetria: myln prende il 50% del tempo, mentre fsin e fcos ciascuna circa il 25%.

Adesso riproviamo utilizzando la funzione mypow() (quella che fa le moltiplicazioni per fare le potenze): basta commentare la linea 6 e scommentare la 7, e ricompilare. Se rifate tutto il percorso otterrete questo callgraph:

Come vedete, le cose sono un po’ cambiate. In particolare, mypow() non chiama alcuna funzione in floating point (come ci si aspetta), e inoltre le percentuali di myln, fsin e fcos sono un po’ cambiate.

È anche possibile mostrare questi numeri direttamente sul sorgente, cliccando sulla tab “Source Code”, ed ecco lo snapshot relativo:

Infine: si può fare una cosa del genera anche per il fortran? Ovviamente sì! Vi riporto semplicemente il comando di compilazione e di esecuzione:

gfortran -g m_c10l.f08 fc10l.f08 -o provaf
valgrind --tool=callgrind ./provaf

E poi tutto come prima. Ecco lo screenshot del callgraph per il fortran:

Qui notiamo che la funzione per la stampa a video (_gfortran_transfer_real_write) prende ben il 13% del tempo, ed è probabilmente lei la causa delle peggiori prestazioni del fortran rispetto al C/C++. Inoltre, notate come da nessuma parte ci sia una chiamata ad alcuna pow(), perché nel fortran l’elevazione a potenza per un numero intero è una operazione del linguaggio.

Ovviamente, questi tool funzionano bene per codice eseguibile, non funzionano affatto bene su Java e su Python. Ci sono però altri tool, più o meno professionali, che fanno cose analoghe. Semmai se ne parla un’altra volta!

Spero che questo post possa esservi utile nel vostro lavoro di “ottimizzatori!”, e ricordate le parole di Knuth!

Nota di Juhan: Mi ricordo il profiler id Borland; questo è molto più bello e anche il prezzo. Vero? Vero dikiyvolk?

Ulteriori confronti, anche con C++


Il colpevole torna sempre sul luogo del delitto, Sherlock Holmes insegna. E allora eccomi di nuovo a confrontare le performance dei linguaggi, continuazione del post Ancora confronti sui linguaggi.

Walter, dikiyvolk, ha tradotto la versione Java in C (c’è un controllo sbagliato alla riga 54), così:

#include <stdio.h>
#include <math.h>

static double f[14];

static double fsin(double a) {
    return a - pow(a, 3.0) / f[3] + pow(a, 5.0) / f[5]
             - pow(a, 7.0) / f[7] + pow(a, 9.0) / f[9]
             - pow(a, 11.0) / f[11] + pow(a, 13.0) / f[13];
}

static double fcos(double a) {
    return 1.0 - pow(a, 2.0) / f[2] + pow(a, 4.0) / f[4]
               - pow(a, 6.0) / f[6] + pow(a, 8.0) / f[8]
               - pow(a, 10.0) / f[10] + pow(a, 12.0) / f[12];
}

static double myln(double x) {
    if (x == 0.0) {
        return -1.0e20;
    } else {
        double r = (x - 1.0) / (x + 1.0);
        return 2.0 * (r + pow(r, 3.0) / 3.0
            + pow(r, 5.0) / 5.0
            + pow(r, 7.0) / 7.0
            + pow(r, 9.0) / 9.0);
    }
}

static double ln10 = myln(10.0);

static double mylog(double x) {
    return x / ln10;
}

int main(int argc, char **argv) {
    int i, j;
    f[0] = 1.0;
    for (i = 1; i < 14; i++)
        f[i] = i * f[i - 1];

    ln10 = myln(10.0);
    int deg = 60 * 60;
    int nsteps = 180 * deg;
    double step = M_PI / nsteps;
    double ssum;
    double csum;
    double a, s, c, t = 0.0;
    double ls, lc;

    for (j = 1; j < 11; j++) {
        ssum = 0.0;
        csum = 0.0;
        for (i = 0; i <= nsteps; i++) {
            a = i * step;
            s = fsin(a);
            ls = mylog(myln(s));
            ssum += s;
            c = fcos(a);
            lc = mylog(myln(c));
            csum += c;

            if ((i % (10 * deg)) == 0) {
                if (c != 0.0)
                    t = s / c;

                printf("%3d %11.8f %11.8f %11.8f %15.8e\n",
                       (i / deg), a, s, c, t);
                printf(" %15.8e %15.8e\n", ls, lc);
            }
        }
    }
    printf("%15.8e\n", ssum);
    printf("%15.8e\n", csum);
}


Come si vede i risultati non sono quelli sperati: incredibilmente lento, 3.55 volte quello del Fortran! C’è qualcosa che non va, sicuro.

G. Lipari ottiene risultati simili. Ma, ricordate i classici: “Quando il gioco si fa duro, i duri cominciano a giocare” (“When the goin’ gets tough the tough get goin’”), Animal House 1978.

E difatti trova il colpevole e propone la soluzione per velocizzare l’esecuzione. La funzione pow() usa double anche per l’esponente, nel caso in esame questo è sempre intero per cui sostituendo tutte le pow() con mypow() definita così definita

inline double mypow(double x, int y) {
    double r = 1;
    for (int i=0; i<y; i++) r *= x;
    return r;
}

Eh sì, semplice adesso che la vedi! xy è nient’altro che x moltiplicato per sé stesso y volte :-)

si ha:

#include <stdio.h>

#ifndef M_PI
#    define M_PI 3.14159265358979323846
#endif

static double f[14];

inline double mypow(double x, int y) {
    double r = 1;
    for (int i=0; i<y; i++) r *= x;
    return r;
}

static double fsin(double a) {
    return a - mypow(a, 3) / f[3] + mypow(a, 5) / f[5]
             - mypow(a, 7) / f[7] + mypow(a, 9) / f[9]
             - mypow(a, 11) / f[11] + mypow(a, 13) / f[13];
}

static double fcos(double a) {
    return 1.0 - mypow(a, 2) / f[2] + mypow(a, 4) / f[4]
               - mypow(a, 6) / f[6] + mypow(a, 8) / f[8]
               - mypow(a, 10) / f[10] + mypow(a, 12) / f[12];
}

static double myln(double x) {
    if (x == 0.0) {
        return -1.0e20;
    } else {
        double r = (x - 1.0) / (x + 1.0);
        return 2.0 * (r + mypow(r, 3) / 3.0
            + mypow(r, 5) / 5.0
            + mypow(r, 7) / 7.0
            + mypow(r, 9) / 9.0);
    }
}

static double ln10 = myln(10.0);

static double mylog(double x) {
    return x / ln10;
}

int main(int argc, char **argv) {
    int i, j;
    f[0] = 1.0;
    for (i = 1; i < 14; i++)
        f[i] = i * f[i - 1];

    ln10 = myln(10.0);
    int deg = 60 * 60;
    int nsteps = 180 * deg;
    double step = M_PI / nsteps;
    double ssum;
    double csum;
    double a, s, c, t = 0.0;
    double ls, lc;

    for (j = 1; j < 11; j++) {
        ssum = 0.0;
        csum = 0.0;
        for (i = 0; i <= nsteps; i++) {
            a = i * step;
            s = fsin(a);
            ls = mylog(myln(s));
            ssum += s;
            c = fcos(a);
            lc = mylog(myln(c));
            csum += c;

            if ((i % (10 * deg)) == 0) {
                if (c != 0.0)
                    t = s / c;

                printf("%3d %11.8f %11.8f %11.8f %15.8e\n",
                       (i / deg), a, s, c, t);
                printf(" %15.8e %15.8e\n", ls, lc);
            }
        }
    }
    printf("%15.8e\n", ssum);
    printf("%15.8e\n", csum);
}


OK, adesso ottengo

Nel mio caso la nuova versione è 4.15 volte più veloce (meno del valore annunciato dal dr.prof. non so se sbaglio qualcosa. O forse il mio ‘puter è meno performante: non sembra ci siano variazioni tra il dichiarare inline o no la mypow().

Un’altra cosa ancora: secondo questa pagina  dovrebbe essere possibile velocizzare la pow() passandogli un intero come secondo parametro; dalle prove fatte non mi risulta.

Ho provato a ricompilare la versione Fortran con l’opzione -O3 ma non ci sono variazioni.

E con Java? Secondo me dovrebbe funzionare. Non mi sembra esista la direttiva inline ma ne so poco.

import static java.lang.System.out;

class jc10l {
    static double f[];

    static double ln10 = myln(10.0);

    public static double mypow(double x, int y) {
        double r = 1;
        for (int i = 0; i < y; i++) r *= x;
        return r;
    }

    public static double fsin(double a) {
        return a - mypow(a, 3) / f[3] + mypow(a, 5) / f[5]
                 - mypow(a, 7) / f[7] + mypow(a, 9) / f[9]
                 - mypow(a, 11) / f[11] + mypow(a, 13) / f[13];
    }

    public static double fcos(double a) {
        return 1.0 - mypow(a, 2) / f[2] + mypow(a, 4) / f[4]
                   - mypow(a, 6) / f[6] + mypow(a, 8) / f[8]
                   - mypow(a, 10) / f[10] + mypow(a, 12) / f[12];
    }

    public static double myln(double x) {
        if(x == 0.0) {
            return -1.0e20;
        } else {
            double r = (x - 1.0) / (x + 1.0);
            return 2.0 * (r + mypow(r, 3) / 3.0
                            + mypow(r, 5) / 5.0
                            + mypow(r, 7) / 7.0
                            + mypow(r, 9) / 9.0);
        }
    }

    public static double mylog(double x) {
        return x / ln10;
    }

    public static void main(String args[]) {
        int i, j;
        f = new double[14];
        f[0] = 1.0;
        for (i = 1; i < 14; i++) {
            f[i] = i * f[i - 1];
        }
        int deg = 60 * 60;
        int nsteps = 180 * deg;
        double step = Math.PI / nsteps;
        double ssum;
        double csum;
        double a, s, c, t = 0.0;
        double ls, lc;

        for (j = 1; j < 11; j++) {
            ssum = 0.0;
            csum = 0.0;
            for (i = 0; i <= nsteps; i++) {
                a = i * step;
                s = fsin(a);
                ls = mylog(myln(s));
                ssum += s;
                c = fcos(a);
                lc = mylog(myln(c));
                csum += c;
                if ((i % (10 * deg)) == 0) {
                    if (c != 0.0) {
                        t = s / c;
                    }
                    out.printf("%3d %11.8f %11.8f %11.8f %15.8e\n",
                          (i / deg), a, s, c, t);
                    out.printf("              %15.8e %15.8e\n",
                               ls, lc);
                }
            }
            out.println(ssum);
            out.println(csum);
        }
    }
}


Ehi! quasi veloce come il C, e comunque più eloce del Fortran!


Ulteriore prova: sostituire l’operatore **con mypow() per il Fortran. Non credo sia il caso di riportare il codice, ecco perché:

Quindi in conclusione (spero questa volta sia davvero tale) il codice C è il più performante. Subito dopo Java e il Fortran. L’operatore ** del Fortran è molto più efficiente di una funzione che esegue un ciclo di moltiplicazioni; oltretutto si paga il costo della chiamata.

C’è poi la domanda (eterna) che continua a non avere una risposta convincente per me: confrontando i listati non trovo che il codice Fortran sia più facile; OK è più simile al Basic e siamo abituati da tempo (per la versione formatata, quella per le schede) ma sono motivi validi e sufficienti?
Adesso mi cacciano :-(

Programmazione C++: mappe

Puntate precedenti: I vettori, ancora sui vettori, Liste e iteratori.

Oggi analizzo un’altro contenitore della libreria standard del C++, molto usato e anche parecchio criticato: la mappa associativa. Si tratta di un contenitore un po’ particolare, perché permette di associare un “valore” a una “chiave”. In pratica, è come se potessimo indirizzare un vettore con una valore qualsiasi invece che con un indice numerico. L’esempio classico è il seguente:

#include <map>
#include <iostream>
#include <string>

using namespace std;

int main()
{
    map<string, int> eta_persona;
    eta_persona["Giuseppe"] = 41;

    pair<string, int> elem1 = {"Giovanni", 35};
    pair<string, int> elem2 = {"Juhan", 21};
    
    eta_persona.insert(elem1);
    eta_persona.insert(elem2);

    cout << "Elem1.first  = " << elem1.first << endl;
    cout << "Elem2.second = " << elem1.second << endl;

    cout << "Eta' di Giuseppe: " << eta_persona["Giuseppe"] << endl;

    cout << "Stampa tutte le eta': " << endl;
    for (auto i : eta_persona) 
        cout << "Eta' " <<  i.first << " = " << i.second << endl;    
}

Alla linea 9 dichiariamo il nostro contenitore e diciamo che associerà numeri interi a stringhe. Nell’esempio, infatti, cerchiamo di memorizzare l’età di un insieme di persone. La chiave è la stringa, ed è quella che utilizzeremo per fare le ricerche nel contenitore; il valore da cercare è l’intero.

Alla linea 10 facciamo subito vedere un esempio di assegnamento: alla chiave “Giuseppe” viene assegnato il valore 41 (e già, sto diventando vecchio…).

Ma esattamente, come vengono memorizzati gli elementi all’interno del contenitore? Gli elementi dentro il contenitore sono coppie (chiave, valore), e una coppia in C++ ha tipo std::pair. Alla linee 12 e 13 prepariamo due coppie di valori (vedete come è giovane Juhan, invece? :) ), e alla linee 15 e 16 li inseriamo nel contenitore. Una pair non è altro che una struttura con due campi: il primo campo si chiama first e corrisponde alla chiave; il secondo campo si chiama (sorpresa!) second e corrisponde al valore. Alle linee 18 e 19 stampiamo i due elementi a video.

Naturalmente, per stampare un elemento del contenitore possiamo anche utilizzare la notazione con le parentesi quadre, come alla linea 21.

Infine, se vogliamo scorrere e stampare tutti gli elementi della mappa, possiamo usare gli iteratori, esattamente come nel vettore e nella lista. Solo che stavolta, un iteratore punta a una coppia. Alle linee 24-25 stampiamo tutti gli elementi della mappa con un ciclo for.

Le persone abituate al vecchio C++ si stupiranno della sintassi: che sono quei due punti? Chi sa programmare in Python invece ritrova una vecchia conoscenza. Il nuovo ciclo for à la Python è disponibile nel nuovo standard ed è parecchio comodo, soprattutto se combinato con la nuova parola chiave auto. Vediamo di spiegarci: il C++ è un linguaggio fortemente tipato (a differenza del Python) e quindi vuole che si dichiarino i tipi di tutte le variabili. La variabile i avrebbe tipo

std::pair<string, int> i;

che però è lungo da scrivere (ed anche noioso). Non solo: il tipo è automaticamente ottenibile dal contenitore che sta a destra dei due punti: se voglio esplorare una map, allora sappiamo già che gli elementi ivi contenuti saranno pair. E allora, perché non lasciare che sia il compilatore a ricavarsi automaticamente il tipo? Questa tecnica è simile a quello che succede in Haskell, che è un linguaggio fortemente tipato, ma dove la dichiarazione dei tipi è superflua, tanto ci pensa il compilatore a ricavarseli automaticamente.

Ok, fin qui tutto facile. Compilate con il solito “g++ -std=c++0x”, otterrete l’eseguibile che potete lanciare, ottenendo questo risultato:

Elem1.first  = Giovanni
Elem2.second = 35
Eta' di Giuseppe: 41
Eta' di Eleonora: 0
Stampa tutte le eta': 
Eta' Eleonora = 0
Eta' Giovanni = 35
Eta' Giuseppe = 41
Eta' Juhan = 21

Uno dei problemi della map è però proprio la sua facilità d’uso, in particolare la notazione con le parentesi quadre. Che succede se ad esempio vogliamo sapere l’età di Eleonora? ecco il programmino:

#include <map>
#include <iostream>
#include <string>

using namespace std;

int main()
{
    map<string, int> eta_persona;
    eta_persona["Giuseppe"] = 41;

    pair<string, int> elem1 = {"Giovanni", 35};
    pair<string, int> elem2 = {"Juhan", 21};
    eta_persona.insert(elem1);
    eta_persona.insert(elem2);

    cout << "Eta' di Giuseppe: " << eta_persona["Giuseppe"] << endl;
    // Attenzione!!
    cout << "Eta' di Eleonora: " << eta_persona["Eleonora"] << endl;
    cout << "Stampa tutte le eta': " << endl;
    for (auto i : eta_persona) 
        cout << "Eta' " <<  i.first << " = " << i.second << endl;    
}

E l’output è questo:

Elem1.first  = Giovanni
Elem2.second = 35
Eta' di Giuseppe: 41
Eta' di Eleonora: 0
Stampa tutte le eta': 
Eta' Eleonora = 0
Eta' Giovanni = 35
Eta' Giuseppe = 41
Eta' Juhan = 21

Ops, l’elemento (“Eleonora”, 0) è stato inserito nella mappa, anche se ho acceduto in lettura! Che è successo?

È successo che i progettisti della libreria si sono trovati con un problema. Volevano fornire la possibilità di accedere agli elementi dell’array con la parentesi quadra, perché faceva “figo”; ma come comunicare il fatto che l’elemento non era presente? L’operazione di accesso non può restituire un valore di errore particolare (come -1) perché proprio quel valore potrebbe essere un numero valido nell’applicazione. Una soluzione poteva essere quella di lanciare un’eccezione, ma il codice si sarebbe complicato molto, e soprattutto, i progettisti volevano tener fuori dalle balle il meccanismo delle eccezioni per quanto possibile. Quindi hanno deciso per una soluzione un po’ sporca: se l’elemento non c’è lo aggiungiamo con un valore di default (nel caso dell’intero, 0). Ed ecco che, semplicemente cercando una chiave dentro la mappa, un nuovo elemento viene magicamente aggiunto.
Purtroppo, questo comportamento è tutt’altro che intuitivo, e se non lo sapete rischiate di finire nei casini.

Per sapere se un elemento esiste all’interno della mappa, senza rischiare di aggiungerlo, dovete utilizzare la funzione find() che vi restituisce un iteratore all’elemento trovato (se esiste), oppure alla fine del contenitore (se non esiste). Ecco il codice corretto:

#include <map>
#include <iostream>
#include <string>

using namespace std;

int main()
{
    map<string, int> eta_persona;
    eta_persona["Giuseppe"] = 41;

    pair<string, int> elem1 = {"Giovanni", 35};
    pair<string, int> elem2 = {"Juhan", 21};
    eta_persona.insert(elem1);
    eta_persona.insert(elem2);
    cout << "Eta' di Giuseppe: " << eta_persona["Giuseppe"] << endl;    

    auto iter = eta_persona.find("Eleonora");
    if (iter != eta_persona.end()) 
        cout <<"L'età di Eleonora è: " << iter->second << ekdl;
    else cout << "Eleonora non è stat ancora inserita" << endl;
}

Notate ancora una volta l’uso di auto per dichiarare l’iteratore. Senza di auto, avrei dovuto scrivere

map<string, int>::iterator iter = eta_persona.find("Eleonora");

Meglio con auto, no? :)

Se lanciate il programmino, ottenete:

Eta' di Giuseppe: 41
Eleonora non è stata ancora inserita

Tutto a posto, dunque.

Vediamo un po’, e se volessi utilizzare la funzione print() scritta l’altra volta per stampare tutti gli elementi di una mappa? Eccola:

#include <map>
#include <iostream>
#include <string>

using namespace std;

template<class Iter>
void print(Iter a, Iter b)
{
    cout << "[";
    for (Iter p=a; p!=b; ++p) cout << *p << ", ";
    cout << "]" << endl;
}

int main()
{
    map<string, int> eta_persona;

    eta_persona["Giuseppe"] = 41;
    pair<string, int> elem1 = {"Giovanni", 35};
    pair<string, int> elem2 = {"Juhan", 21};    
    eta_persona.insert(elem1);
    eta_persona.insert(elem2);
    cout << "Eta' di Giuseppe: " << eta_persona["Giuseppe"] << endl;

    cout << "Stampa tutte le eta': " << endl;
    print(eta_persona.begin(), eta_persona.end());
}

Purtroppo, così non funziona, il gcc vi da questo errore (quasi) incomprensibile:

maps2.cpp: In function ‘void print(Iter, Iter) [with Iter = std::_Rb_tree_iterator<std::pair<const std::basic_string, int> >]’:
maps2.cpp:27:49:   instantiated from here
maps2.cpp:11:31: error: cannot bind ‘std::ostream {aka std::basic_ostream}’ lvalue to ‘std::basic_ostream&&’
/usr/include/c++/4.6/ostream:581:5: error:   initializing argument 1 of ‘std::basic_ostream& std::operator<<(std::basic_ostream&&, const _Tp&) [with _CharT = char, _Traits = std::char_traits, _Tp = std::pair<const std::basic_string, int>]’

(Si lo so, è brutto, ma ora ve lo spiego io).
In pratica, alla linea 11 stiamo cerando di stampare una coppia (l’elemento puntato da p, che è un iteratore alla mappa), e il C++ non sa come stamparla. La funzione di stampa (sarebbe quell’operatore strano, <<) non sa come trattare le coppie, e quindi si ferma dando errore di compilazione.

Niente panico! Se il C++ non sa fare una cosa da se’, basta spiegargliela per bene. Non sai stampare una pair? Te lo dico io come si fa, aggiungendo una funzione in più:

template<class F, class S>
std::ostream& operator<<(std::ostream &out, 
                         const std::pair<F,S> &elem)
{
    out << "(" << elem.first << ", " << elem.second << ")";
    return out;
}

Questo è il famigerato overload degli operatori. In pratica, l’operatore << non è altro che una funzione, il cui nome è operator<<, e prende due argomenti, di cui il primo è uno stream di uscita (ad esempio cout, ma anche un qualsiasi file), il secondo è l’oggetto da stampare, nel nostro caso una qualunque coppia di elementi qualsiasi, per esempio di tipo generico F e S. Poiché una funzione così non esiste, la scriviamo noi. E gli diciamo che deve stampare per prima cosa una parentesi tonda aperta, seguita dal primo elemento della coppia, dalla virgola, dal secondo elemento della coppia, e infine dalla parentesi tonda chiusa. Infine, ritorniamo lo stream come risultato, in maniera che si possa fare una concatenazione, per esempio con endl.

Chiaro? No? Non importa, sarà chiaro con degli altri esempi che faremo più avanti (spero). Ecco il programma finale:

#include <map>
#include <iostream>
#include <string>

using namespace std;

template<class F, class S>
std::ostream& operator<<(std::ostream &out, 
                         const std::pair<F,S> &elem)
{
    out << "(" << elem.first << ", " << elem.second << ")";
    return out;
}

template<class Iter>
void print(Iter a, Iter b)
{
    cout << "[";
    for (Iter p=a; p!=b; ++p) cout << (*p) << ", ";
    cout << "]" << endl;
}


int main()
{
    map<string, int> eta_persona;

    eta_persona["Giuseppe"] = 41;
    pair<string, int> elem1 = {"Giovanni", 35};
    pair<string, int> elem2 = {"Juhan", 21};    
    eta_persona.insert(elem1);
    eta_persona.insert(elem2);
    cout << "Eta' di Giuseppe: " << eta_persona["Giuseppe"] << endl;

    cout << "Stampa tutte le eta': " << endl;
    print(eta_persona.begin(), eta_persona.end());
}

Prima di chiudere, alcune considerazione sull’efficienza.
La mappa è implementata internamente come un albero binario di ricerca, per cui ogni accesso prende un tempo proporzionale al logaritmo in base due del numero di elementi presenti. Anche per l’inserzione è lo stesso, sempre tempo logaritmico. Dato che l’albero binario è già ordinato, non ha senso ordinare una mappa; il requisito fondamentale, però, è che esista un ordinamento per il tipo che rappresenta la chiave. Se la chiave è una stringa, come nei nostri esempi, l’ordinamento di default è quello alfabetico. E’ però possibile sia cambiare l’ordinamento di default, sia impostare un ordinamento di default per una vostra classe specifica. Ma questo lo vedremo la prossima volta, che adesso si è fatto tardi!

Buon luglio e accendete i condizionatori che i PC soffrono al caldo!

Programmazione C++: liste e iteratori

Le liste

Nei post sulla programmazione funzionale ho rotto le scatole con le liste, la struttura dati di base di ogni buon linguaggio funzionale che si rispetti. Ci sono le liste in C++? Si, ma non sono poi così flessibili. In realtà, come vedremo più avanti, ogni contenitore può essere trattato come una lista, perché il C++ generalizza il concetto in una maniera molto potente e flessibile.

Dicevo nello scorso post che i vettori non sono sempre la struttura dati più adatta per le nostre esigenze, specialmente quando dobbiamo inserire e estrarre dal mezzo, perché gli elementi sono memorizzati consecutivamente in memoria e per inserire nel mezzo devo spostare una buona metà di elementi.

Nella lista, invece, gl elementi non sono memorizzati consecutivamente in memoria, ma possono stare dove gli pare. In pratica, il contenitore list della libreria standard implementa la buona vecchia lista linkata, gioia e dolori degli studenti del corso di “Fondamenti di Informatica I”.

Facciamo subito un esempio che è meglio.

#include <list>
#include <iostream>

using namespace std;

int main()
{
    list<int> lista;

    for (int i=0; i<10; i++) 
        lista.push_back(i);

    for (int i=0; i<10; i++) {
        cout << "Elemento " << i << ": " << lista.front() << endl;
        lista.pop_front();
    }
    return 0;
}

Per prima cosa, stavolta includiamo l’header file list invece di vector. Con il primo ciclo for inseriamo 10 elementi in fondo alla lista. Con il secondo ciclo for, prima stampiamo a video il contenuto della testa della lista (ottenuta con la funzione front), e poi lo estraiamo (con la funzione pop_front()).

Se volessi leggere gli elementi a partire dal fondo, posso usare back() e push_back(), come segue:

#include <list>
#include <iostream>

using namespace std;

int main()
{
    list<int> lista;

    for (int i=0; i<10; i++) 
        lista.push_back(i);

    for (int i=0; i<10; i++) {
        cout << "Elemento " << i << ": " << lista.back() << endl;
        lista.pop_back();
    }
    return 0;
}

E se volessi leggere un elemento nel mezzo? Qui non si può usare l’accesso diretto (o random) come nel vettore. In altre parole, scrivere

cout << lista[5];

darebbe un errore di compilazione perché la lista non ha la possibilità di visitare il quinto elemento con una somma e una moltiplicazione, ma deve scorrere tutti gli elementi uno alla volta.

Inoltre, entrambi questi programmi stampano la lista, ma la distruggono man mano che leggono. non è molto comodo vorremmo poter leggere senza distruggere.

Iteratori

Per scorrere gli elementi si usa il concetto di iteratore, che è anche un design pattern, ovvero una tecnica di programmazione ben studiata nei sacri testi della programmazione object oriented. Cos’è un iteratore?
In OOP, l’iteratore è un oggetto che permette di visitare gli elementi in un contenitore in un certo ordine. L’iteratore disaccoppia il contenitore dal metodo di visita, e semplifica l’interfaccia (un sacco di paroloni, ma poi in pratica è molto semplice, vedrete).

In C++ l’iteratore è una generalizzazione del concetto di puntatore, che ci permette di scorrere gli elementi di un contenitore in ordine, utilizzando una sintassi simile a quella dei puntatori. Ecco come scorrere gli elementi di una lista.

#include <list>
#include <iostream>

using namespace std;

int main()
{
    list<int> lista;

    for (int i=0; i<10; i++) 
        lista.push_back(i);

    list<int>::iterator p;
    int i = 0;
    for (p = lista.begin(); p != lista.end(); p++) 
        cout << "Elemento " << i++ << ": " << *p << endl;  

    return 0;
}

(Sembra molto complicato a prima vista, ma vedrete che tra un po’ ve lo semplifico. E’ che all’inizio, per capire le cose per bene, bisogna prendere la strada lunga, poi quando le nostre gambe sono ben allenate possiamo prendere la scorciatoia).

Quindi, l’iteratore qui l’ho chiamato p, e che si tratti di un iteratore lo capite dalla dichiarazione del tipo:

list::iterator p;

Poi, nel ciclo for, p prende inizialmente il valore restituito dalla funzione begin(). Questa funzione (metodo in dialetto OOP) restituisce l’iteratore che punta al primo elemento della lista. Il ciclo for si ferma quando p è pari al valore della funzione end(), che restituisce l’iteratore che punta al valore successivo all’ultimo elemento della lista. In pratica, end() restituisce un puntatore a un elemento non valido. A ogni iterazione del ciclo for, il valore di p è incrementato (p++): questo vuol dire che al termine di ogni iterazione, p passerà da un elemento al successivo.

Per ottenere il valore dell’elemento, basta utilizzare l’indirezione, ovvero l’operatore *:

cout << "Elemento " << i++ << ": " << *p << endl;  

Ok, fin qui credo sia semplice da capire, anche se un po’ complicato da scrivere.
E adesso, sorpresa: gli iteratori esistono anche per i vettori!
Ecco l’esempio:

#include <vector>
#include <iostream>

using namespace std;

int main()
{
    vector<int> vettore;

    for (int i=0; i<10; i++) 
        vettore.push_back(i);

    vector<int>::iterator p;
    int i = 0;
    for (p = vettore.begin(); p != vettore.end(); p++) 
        cout << "Elemento " << i++ << ": " << *p << endl;  

    return 0;
}

Come vedete ho semplicemente sostituito list con vector, e lista con vettore, e tutto funziona come prima.

Ok, se mi avete seguito fin qui, adesso è venuto il momento delle magie, attenzione. Per prima cosa, scrivo una funzione print che stampa tutti gli elementi di una lista:

void print(list<int>::iterator a, list<int>::iterator b)
{
    list<int>::iterator p;
    cout << "[";
    for (p=a; p!=b; ++p) cout << *p << ", ";
    cout << "]" << endl;
}

Adesso per stampare una lista mi basta scrivere:

print(lista.begin(), lista.end());

Però, se volessi stampare un vettore dovrei riscrivere la funzione… posso generalizzarla? Certo che sì, basta usare gli ingiustamente famigerati template:

template<class Iter>
void print(Iter a, Iter b)
{
    cout << "[";
    for (Iter p=a; p!=b; ++p) cout << *p << ", ";
    cout << "]" << endl;
}

Ecco, adesso la funzione non dipende più dal fatto che ho una lista: basta che abbia una qualsiasi cosa che si comporti come un iteratore. Qualsiasi cosa significa anche un normale puntatore C! Ecco un esempio finale in cui metto tutto insieme:

#include <list>
#include <vector> 
#include <string> 
#include <deque> 
#include <iostream>

using namespace std;

template<class Iter>
void print(Iter a, Iter b)
{
    cout << "[";
    for (Iter p=a; p!=b; ++p) cout << *p << ", ";
    cout << "]" << endl;
}


int main()
{
    list<int> lista;
    vector<int> vettore;

    for (int i=0; i<10; i++) 
        lista.push_back(i);
    for (int i=0; i<15; i++) 
        vettore.push_back(2*i);

    deque<string> coda = {"Ok", "Panico", "insalate", "di", "cibernetica"}; 
    int array[] = {1,2,3,5,7,11,13,17};

    string str = "C++ rulez";

    print(lista.begin(), lista.end());
    print(vettore.begin(), vettore.end());
    print(coda.begin(), coda.end());
    print(array, array+8);
    print(str.begin(), str.end());

    return 0;
}

Se salvate questo file con il nome “stampa_tutto.cpp”, potete compilare con il comando:

g++ -std=c++0x stampa_tutto.cpp

Questo produce il file a.out, e lanciandolo otterrete il seguente output:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, ]
[Ok, Panico, insalate, di, cibernetica, ]
[1, 2, 3, 5, 7, 11, 13, 17, ]
[C, +, +,  , r, u, l, e, z, ]

Spiegazione breve (che si è fatto tardi):
Ho preparato 4 contenitori: una lista, un vettore, una coda (con il tipo deque), e un classico array C. Le ho inizializzate in modo diverso (con un ciclo for le prime due, con una inizializzazione in linea le ultime 2).
Infine, ho preparato una stringa str.

Quindi, ho chiamato print() su tutte. L’unica chiamata strana è quella per l’array C: poichè un array non è un oggetto C++ ma una struttura dati C, non ha le funzioni membro begin() e end(). Quindi, ho passato direttamente il puntatore al primo elemento (array) e il puntatore successivo all’ultimo elemento (array+8).

Infine: anche la stringa è un contenitore! In particolare, è un contenitore di caratteri! Possiede gli iteratori, e le funzioni begin() e end(), come il vettore e la lista. Per cui, non vedo perchè non potrei chiamare la mia funzione print() su di essa, non credete?

Cone questo, oggi è tutto, alla prossima!

Programmazione C++: ancora sui vettori

Oggi si parlerà persino di Facebook, pensa un po’.

Qualche tempo fa (troppo!) avevamo visto i vettori della libreria standard del C++, e di come assomigliassero agli array del C, e di come fossero in fondo semplici da utilizzare. Abbiamo appena scalfito un granellino della libreria standard, oggi ne scalfiamo un altro.

Memorizzare gli elementi dentro un vettore

Il vettore del C++ è bello, ma non è detto che sia sempre la maniera ottimale di gestire un insieme di elementi, per cui la libreria standard mette a disposizione molti altri contenitori, tra cui le liste. Che differenza c’è tra i vari contenitori?

Il vettore ha un requisito fondamentale: deve essere quanto più possibile simile all’array del C. Nell’array, gli elementi sono memorizzati in maniera consecutiva in memoria, in maniera da poter accedere velocemente a un qualsiasi elemento. Ad esempio, supponiamo di avere un array di 10 interi: un intero sono 4 byte, quindi in memoria l’array richiede 40 byte. Questo blocco di 40 byte viene conservato in memoria in un unico blocco consecutivo di indirizzi lungo 40 bytes. Se il primo elemento sta all’indirizzo X, il secondo starà all’indirizzo X+4, il terzo all’indirizzo X+8, l’i-esimo all’indirizzo X+4*(i-1). Se non ci credete, ecco un programmino che ve lo dimostra:

#include <stdio.h>

int main()
{
    int array[10];

    printf("%p\n", &array[0]);
    printf("%p\n", &array[1]);
    printf("%p\n", &array[2]);
    long x = (long) &array[1];
    long y = (long) &array[0];

    printf("Difference: &array[1] - &array[0] = %ld\n", x-y);
}

L’output sarà qualcosa del tipo:

0x7fff2b98a1a0
0x7fff2b98a1a4
0x7fff2b98a1a8
Difference: &array[1] - &array[0] = 4

Ok, adesso credo che sarà facile capire come il C/C++ traduce l’espressione array[i]; prende l’indirizzo base di array, e ci aggiunge (i-1) moltiplicato per la dimensione del tipo di dati.
Questo tipo di accesso si chiama accesso random perché con una somma e una moltiplicazione arriviamo all’elemento voluto.

Stessa cosa avviene per il vettore, e invito voi stessi a fare la prova modificando opportunamente il programma precedente per usare un vector invece dell’array.

C’è un però: il vettore, al contrario dell’array, ha dimensione dinamica, si può allargare e stringere a volontà, aggiungendo e togliendo elementi. Per esempio, possiamo continuare ad aggiungere elementi in fondo al vettore con la funzione push_back(). Ci sono però due problemi: 1) non sappiamo a priori quanto spazio allocare a un vettore; 2) nel caso in cui lo spazio allocato non basti, bisogna spostare il vettore in uno blocco di memoria più grande. Vediamolo con un esempio:

#include <vector>
#include <iostream>

using namespace std;

int main()
{
    vector<int> v1(16); // alloca inizialmente spazio per 16 elementi
    vector<int> v2;     // alloca lo spazio di default

    cout << "numero elementi nel vettore v1: " << v1.size() << endl;
    cout << "spazio nel vettore v1: " << v1.capacity() << endl;
    cout << "numero elementi nel vettore v2: " << v2.size() << endl;
    cout << "spazio nel vettore v2: " << v2.capacity() << endl;
    cout << endl;

    v2.push_back(13);
    cout << "numero elementi nel vettore v2: " << v2.size() << endl;
    cout << "spazio nel vettore v2: " << v2.capacity() << endl;
    cout << "Indirizzo primo elemento: " << &v2[0] << endl;
    cout << endl;

    v2.push_back(17);
    cout << "numero elementi nel vettore v2: " << v2.size() << endl;
    cout << "spazio nel vettore v2: " << v2.capacity() << endl;
    cout << "Indirizzo primo elemento: " << &v2[0] << endl;
    cout << endl;

    v2.push_back(19);
    cout << "numero elementi nel vettore v2: " << v2.size() << endl;
    cout << "spazio nel vettore v2: " << v2.capacity() << endl;
    cout << "Indirizzo primo elemento: " << &v2[0] << endl;
    cout << endl;

    v2.push_back(23);
    cout << "numero elementi nel vettore v2: " << v2.size() << endl;
    cout << "spazio nel vettore v2: " << v2.capacity() << endl;
    cout << "Indirizzo primo elemento: " << &v2[0] << endl;
    cout << endl;
}

L’output è questo (a meno dei valori degli indirizzi, naturalmente!):

numero elementi nel vettore v1: 16
spazio nel vettore v1: 16
numero elementi nel vettore v2: 0
spazio nel vettore v2: 0

numero elementi nel vettore v2: 1
spazio nel vettore v2: 1
Indirizzo primo elemento: 0xf20060

numero elementi nel vettore v2: 2
spazio nel vettore v2: 2
Indirizzo primo elemento: 0xf20080

numero elementi nel vettore v2: 3
spazio nel vettore v2: 4
Indirizzo primo elemento: 0xf20060

numero elementi nel vettore v2: 4
spazio nel vettore v2: 4
Indirizzo primo elemento: 0xf20060

Avete capito come funziona? Innanzitutto, è possibile specificare una capacità iniziale che per il vettore v1 abbiamo stabilito essere pari a 16 elementi. Invece, per il vettore v2 non abbiamo specificato niente, quindi la capacità iniziale è pari a 0. Quando iniziamo a inserire elementi in v2, utilizzando la funzione push_back(), a poco a poco la capacità cresce. Inizialmente segue il numero di elementi: prima 1 elemento, e 1 di capacità; poi, con due elementi abbiamo 2 di capacità; con 3 elementi, invece, il vettore si allarga un po’ di più, e arriva a 4; a quel punto, quando inseriamo il quarto elemento non c’è bisogno di allargare ancora. Cosa succederebbe se inserissimo un quinto elemento? a quanto viene portata la capacità?(soluzione in fondo al post).

Guardiamo anche l’indirizzo del primo elemento: come vedete, cambia quando passiamo da 1 a 2; questo perché potrebbe non esserci spazio libero dopo il primo elemento per metterci il secondo; può darsi che quello spazio sia già utilizzato da qualche altra cosa. Quindi, il vettore fa così:

  • Alloca un nuovo spazio di memoria in modo da poter contenere anche il nuovo elemento (e magari un po’ di più);
  • Copia tutti gli elementi dal precedente blocco di memoria in quello nuovo;
  • Dealloca il vecchio blocco, che ormai non serve più;
  • Infine, inserisce il nuovo elemento

Come vedete è un’operazione piuttosto pesante, specialmente se il vettore è grande. Immaginate poi cosa vuol dire inserire un elemento nel mezzo, per esempio alla posizione i-esima:

  • Spostare tutti gli elementi dall’i-esimo in poi di un posto in avanti
  • inserire il nuovo elemento al posto i-esimo

Anche cancellare un elemento nel mezzo comporta analogamente delle copie.

Per cui, se dovete fare un sacco di inserimenti e/o estrazioni dal mezzo, non è che il vettore sia un contenitore molto efficiente. Ne esistono altri come la lista e la map che vedremo le prossime puntate.

Per ora ritorniamo alla mia domandina di prima: di quanto si allarga la capacità del vettore v2 quando inseriamo un quinto elemento? Inserite il seguente codice alla fine della funzione main, e provate a vedere che stampa:

    v2.push_back(29);
    cout << "numero elementi nel vettore v2: " << v2.size() << endl;
    cout << "spazio nel vettore v2: " << v2.capacity() << endl;
    cout << "Indirizzo primo elemento: " << &v2[0] << endl;
    cout << endl;

Ed ecco le ultime 3 righe stampate su schermo:

numero elementi nel vettore v2: 5
spazio nel vettore v2: 8
Indirizzo primo elemento: 0x174f0a0

Avete capito la regola? La capacità del vettore ogni volta raddoppia! Se continuate a inserire elementi, al nono elemento inserito la capacità diventerà 16, e così via.
Questa regola serve a ridurre il numero di operazioni di allocazione/copia/deallocazione in cambio di un po’ di spazio sprecato (fino al doppio nel caso peggiore). Il numero di operazioni di allocazione/copia/deallocazione è infatti proporzionale al logaritmo in base 2 del numero di inserimenti.

Il raddoppio però non è necessariamente una regola ottimale. Per esempio, i programmatori di folly (la libreria C++ di Facebook, recentemente rilasciata open source) utilizza una regola diversa. Qui trovate la descrizione della loro regola, e la spiegazione del perché hanno fatto così.

Alla prossima!

Iscriviti

Ricevi al tuo indirizzo email tutti i nuovi post del sito.

Unisciti agli altri 37 follower