Oppure

Loading
07/09/18 16:45
AldoBaldo
"Ispirato" dalla discussione sulle "matrici" propostaci da comtel, ho voluto provare a mettere insieme un template che si occupi (in C++, ovviamente) di quel tipo di costrutto, ovvero di "finte" matrici implementate in memoria come vettori. Siccome è la prima volta che metto le mani sui template, mi farebbe piacere sapere se ho applicato correttamente quel che avrei dovuto applicare correttamente. Intendo, il codice sembra funzionare ma... mi sfugge qualche particolare importante che dovrei conoscere e del quale dovrei tenere conto?

#ifndef TEMPLATE_MATRICE_H
#define TEMPLATE_MATRICE_H

#include <stdlib.h>

template<class TIPO>
class MATRICE {
    public:
        MATRICE();
        MATRICE( size_t colonne, size_t righe );
        virtual ~MATRICE();
        MATRICE(const MATRICE& other);

        // modifica
        bool Dimensiona(
            size_t colonne, size_t righe, bool preserva_dati = true );

        // lettura
        size_t QColonne( void ) const { return qc; }
        size_t QRighe( void )   const { return qr; }
        size_t QCelle( void )   const { return qc*qr; }

        MATRICE& operator=(const MATRICE& other);
        TIPO& operator()( size_t x, size_t y );

        static const char *kStrErrNoDim;
        static const char *kStrErrNoCoo;

    protected:

    private:
        TIPO *m;    // l'array che contiene la matrice
        size_t qr;  // quantita' di righe
        size_t qc;  // quantita' di colonne
};

template<class TIPO>const char *MATRICE<TIPO>::kStrErrNoDim =
    "template MATRICE: dimensionamento fallito";

template<class TIPO>const char *MATRICE<TIPO>::kStrErrNoCoo =
    "template MATRICE: coordinate esterne alla matrice";

template<class TIPO>
MATRICE<TIPO>::MATRICE() {
    m = NULL;
    qc = qr = 0;
}

template<class TIPO>
MATRICE<TIPO>::MATRICE( size_t colonne, size_t righe ) {
    m = NULL;
    qc = qr = 0;

    if( !Dimensiona(colonne,righe) ) throw kStrErrNoDim;
}

template<class TIPO>
MATRICE<TIPO>::~MATRICE() {
    if( m ) delete[] m;
}

template<class TIPO>
bool MATRICE<TIPO>::Dimensiona(
    size_t colonne, size_t righe, bool preserva_dati ) {

    try {
        TIPO *mAux = new TIPO[colonne*righe];

        if( preserva_dati ) {
            for( size_t max_rig=righe<qr?righe:qr, r=0; r<max_rig; ++r )
                for( size_t max_col=colonne<qc?colonne:qc, c=0; c<max_col; ++c )
                    mAux[r*max_col+c] = m[r*qc+c];
        }

        if( m ) delete[] m;

        m  = mAux;
        qc = colonne;
        qr = righe;

        return true;
    } catch( ... ) {
        return false;
    }
}

template<class TIPO>
MATRICE<TIPO>::MATRICE(const MATRICE& other) {
    m = NULL;
    qc = qr = 0;
    *this = other;
}

template<class TIPO>
MATRICE<TIPO>& MATRICE<TIPO>::operator=(const MATRICE<TIPO>& rhs) {
    if (this == &rhs) return *this; // handle self assignment

    if( !Dimensiona(rhs.qc,rhs.qr,false) ) throw kStrErrNoDim;

    for( size_t r=0; r<qr; ++r )
        for( size_t c=0; c<qc; ++c )
            m[r*qc+c] = rhs.m[r*qc+c];

    return *this;
}

template<class TIPO>
TIPO& MATRICE<TIPO>::operator()( size_t x, size_t y ) {
    if( x>=qc||y>=qr ) throw kStrErrNoCoo;
    return m[y*qc+x];
}

#endif // TEMPLATE_MATRICE_H


EDIT: siccome queste cose le conservo per poi riutilizzarle magari anche dopo un bel po' di tempo, mi son preparato un file di promemoria che ho messo in tinyurl.com/…
Ultima modifica effettuata da AldoBaldo 07/09/18 19:44
ATTENZIONE! Sono un hobbista e l'affidabilità delle mie conoscenze informatiche è molto limitata. Non prendere come esempio il codice che scrivo, perché non ho alcuna formazione accademica e rischieresti di apprendere pratiche controproducenti.
07/09/18 21:35
TheDarkJuster
Nell'operatore di copia ti sei dimenticato di liberare la memoria attualmente utilizzata prima di fare la copia profonda....
aaa
07/09/18 22:24
AldoBaldo
Parli del costruttore di copie (MATRICE(const MATRICE& other);, riga 12) o dell'operatore "=" (riga 23)? In entrambi i casi dovrebbe farsene carico il "if( m ) delete[] m;" che c'è alla riga 75, nell'ambito del metodo Dimensiona(). Almeno, questo secondo le mie intenzioni. Mi sfugge qualcosa? Non riesco a vedere cosa.
ATTENZIONE! Sono un hobbista e l'affidabilità delle mie conoscenze informatiche è molto limitata. Non prendere come esempio il codice che scrivo, perché non ho alcuna formazione accademica e rischieresti di apprendere pratiche controproducenti.
07/09/18 23:23
TheDarkJuster
Hai ragione, non lo avevo visto.

Allora piccola nota: sposta quel pezzo di codice in una funzione apposita, così organizzi meglio il codice e attinenti la leggibilità.
aaa
08/09/18 5:52
AldoBaldo
Tipo un metodo privato chiamato, che so, "DistruggiOggettiDinamici()"? Qualche nome migliore ti viene in mente?

Riguardando il codice...

1) Pensavo di rendere inline l'overload dell'operatore "()", quindi ho fatto un po' di test e... sorpresa!... le prestazioni non cambiano d'una virgola neppure nel caso di "matrici" molto grandi e cicli di ripetizioni d'accesso enormi. Non è strano? Da quel che ho sempre letto, i metodi inline dovrebbero dar luogo a codice più "ponderoso" in termini di dimensioni ma più celere in termini di prestazioni. Mah...

Ancora...

2) Ho cambiato il codice di quell'operatore in questo modo:

// vecchia versione
if( x>=qc||y>=qr ) throw kStrErrNoCoo;
    return m[y*qc+x];

// nuova versione
if( x<qc&&y<qr ) return m[y*qc+x];
    throw kStrErrNoCoo;


Anche qui, stando a quel che ho letto da più di una fonte mi aspettavo un minimo quid di miglioramento nelle prestazioni, ma dai test non è cambiato assolutamente nulla.

Ultima cosa...

3) Mi sto chiedendo se non sia il caso di "isolare" la copia dei vecchi dati in un apposito try per intercettare le eventuali eccezioni lanciate dagli oggetti copiati e "convertirle" in eccezioni legate alla "matrice" anziché alla classe degli oggetti copiati stessi. Tu che faresti?

(che bella cosa i forum dove ti rispondono senza darti della bestia ogni tre per due! :heehee:)
ATTENZIONE! Sono un hobbista e l'affidabilità delle mie conoscenze informatiche è molto limitata. Non prendere come esempio il codice che scrivo, perché non ho alcuna formazione accademica e rischieresti di apprendere pratiche controproducenti.
08/09/18 13:48
lumo
Trovo personalmente poco utile poter ridimensionare la matrice mantenendo i dati interni, comunque se vuoi liberarti di quei try/catch che al tuo stile molto C-oso secondo me non si adattano potresti provare ad usare nothrow (googla, praticamente permette di ritornare nullptr invece di avere un errore).

Invece di farti gli errori tuoi potresti anche pensare ad usare cplusplus.com/reference/stdexcept/out_of_range/

Riguardo a 1), il costo di una chiamata a funzione non è così grande, comunque può essere che il compilatore abbia ottimizzato il tuo codice rendendole inline lo stesso (l'indicazione inline non è obbligatoria per un compilatore) oppure che il tuo codice di test non sia corretto.

Perché ti aspetti un miglioramento delle prestazioni da 2?
aaa
09/09/18 1:06
TheDarkJuster
AldoBaldo, un consiglio spassionato: scrivi codice con buon gusto e stile, lascia fuori il superfluo, mantieni i dati relazionati il più vicino possibile.... Il resto non ha senso. Non usare inline se non è un requisito estremamente necessario per la funzionalità: i compilatori hanno dietro decine e decine di persone pronte a fare tutto il necessario pergenerare codice il più veloce possibile ed onestamente potresti scoprire che i tuoi sforzi sono oltre che inutili, potrebbero essere controproducenti.
aaa
09/09/18 7:43
AldoBaldo
Apposta ho previsto la possibilità di NON copiare i dati in fase di ridimensionamento: basta aggiungere un terzo parametro (false) a Dimensiona() e il gioco è fatto. Ora che mi fai rileggere il codice, mi accorgo che alla riga 54 (nella funzione creatrice parametrizzata con il numero delle colonne e delle righe) ci sta bene l'aggiunta di quel terzo parametro false alla chiamata "interna" a Dimensiona(). E' ben vero che i cicli verrebbero saltati comunque, però si sostituisce una certa quantità di operazioni col semplice controllo per verificare se preserva_dati è true o false. Senza tornarci non me ne sarei accorto.

Da qui in poi, DEVO premettere che so di avere lacune consistenti con il C++, quindi le mie affermazioni sono tutte a livello di speculazione di chi sta cercando di capire delle cose che si addentrano su un terreno semi-ignoto.

Ho controllato out_of_range. In effetti aggiungerebbe il vantaggio di fare riferimento a un'eccezione standard, però obbligherebbe a includere stdexcept. Il che pare comporti la "iniezione" di una valangata di roba "implicita" nell'eseguibile (ho controllato: un programmino di una manciata di righe, compilato, balza da 113 byte a 720 byte semplicemente aggiungendo un catch che fa riferimento a std::out_of_range!).

#include <stdio.h>
#include "template_matrice.h"

int main() {
    try {
        MATRICE<int> m(10,10);
        m(9,11) = 31; // secondo parametro esterno alla matrice
    } catch( const char *e ) {
        puts( e );
    } catch( const std::out_of_range& oor ) {
        puts( oor.what() );
    }

    return 0;
}

// se in template_matrice.h c'e' throw std::out_of_range(kStrErrNoCoo):
// "Output file is bin\Release\prova out_of_range.exe with size 720.50 KB"

// se in template_matrice.h c'e' throw kStrErrNoCoo e basta:
// "Output file is bin\Release\prova out_of_range.exe with size 113.50 KB"


Di out_of_range non avevo mai sentito parlare, mentre nothrow già mi era noto. Ti ringrazio per la segnalazione, ma se non ci sono differenze funzionali tra usarlo e non usarlo, in un contesto come questo credo di potermela cavare anche con le "complicazioni" del try/catch. In altri contesti, in effetti, l'ho trovato molto comodo e l'ho usato senza remore perché mi permetteva di aggirare alcune delle mie lacune (dubbi, più che altro; così, per andare sul sicuro...). Consigli il nothrow? Ci metto un attimo a cambiare la formulazione, e sai che di te mi fido.

Sull'ultimo punto... Il codice di test era, banalmente, una serie "ponderosa" di cicli che accedevano ai dati in matrice in lettura e in scrittura, posta tra due clock() per rilevare il tempo in millisecondi. Molto grezzo e poco affidabile, lo so, anche perché quasi sicuramente deve fare i conti con la coesistenza con quel che succede in chissà quanti processi attivi sul computer che "inquinano" il risultato in modi per me del tutto imprevedibili. Diciamo che è un metodo MOLTO spannometrico.

Dal punto 2) mi sarei aspettato delle differenze perché sul testo sul quale studiai anni fa riportava che è preferibile ("più efficiente";) mettere il caso che si presenta più spesso nell'if piuttosto che nell'else. Secondo la spiegazione che dava sul testo, l'if viene valutato sempre e comunque, mentre l'else viene valutato solo se l'if non è valido. Noto ora che non ho inserito alcun else, e che si arriva al throw semplicemente se "fallisce" l'if, senza alcun ulteriore controllo (quell'if viene valutato comunque). MAGARI sarebbe stato diverso se avessi scritto if( x<qc&&y<qr ) return m[y*qc+x]; ELSE throw kStrErrNoCoo. Ma else non c'è. Vale il ragionamento o sto travisando/speculando sul sesso degli angeli?
ATTENZIONE! Sono un hobbista e l'affidabilità delle mie conoscenze informatiche è molto limitata. Non prendere come esempio il codice che scrivo, perché non ho alcuna formazione accademica e rischieresti di apprendere pratiche controproducenti.