Home ] Su ] Novità ] FAQ ] Hardware ] Software ] Windows ] Sicurezza ] Linguaggi ] Cerca ] Contatta ] Guestbook ] Mappa ] Info su... ]

Le FAQ sul linguaggio C++ - Pagina 3


La versione originale di questa FAQ è sempre disponibile all'indirizzo http://www.penguinpowered.com/~The_Cpp_Compass/.
Nel suddetto indirizzo potete trovare materiale più aggiornato rispetto al contenuto della FAQ sul linguaggio C++ di questo sito.
Prima di continuare a leggere le seguenti sezioni della FAQ sul linguaggio C++, vi invitiamo a prendere visione del disclaimer, dei copyright e delle note di revisione, situate in questa pagina.





Le Frequently Asked Questions di it.comp.lang.c++ a cura di Roberto Gerola


Puntatori

Come dichiaro un puntatore a funzione e come richiamo quest'ultima ?

Dipende che argomenti prende, e che tipo di valore ritorna, la funzione che si vuole chiamare. Ad esempio, a una funzione che prende un double e torna un int si potrebbe dichiarare un puntatore così (tutto quanto segue vale nell'ipotesi che si voglia effettivamente un puntatore a funzione, non un puntatore-a-metodo che è tutta un'altra cosa; in altri termini, questo vale per funzioni libere, o membri statici di una qualche classe, non per metodi di un oggetto, che hanno sintassi e semantica drasticamente diversi):

MiaClasse::Funzione(int (*ilpunt)(double))

e chiamarlo, ad esempio, così:

int risult = ilpunt(23.45);

Come sempre in C e C++, le dichiarazioni con sintassi strana è meglio gestirle con un typedef:

typedef int (*pfun_double_int_t)(double);

MiaClasse::Funzione(pfun_double_int_t ilpunt)

comunque, è solo una questione di stile, manutenibilità, e leggibilità -- il codice generato sarà esattamente lo stesso.
Volendo, in fase di chiamata, si può preporre al "ilpunt" una operatore di dereferencing, cioè chiamare:

int risult = (*ilpunt)(23.45);

ma non avrebbe particolarmente senso farlo (si può mettercene piu` di uno, volendo) -- è decisamente più leggibile omettere l'asterisco e relative parentesi, e la semantica è rigorosamente identica.
E per i puntatori a metodi di un oggetto come faccio ?
Non si fa: un puntatore-a-metodo è sempre su di una specifica classe (non esiste, insomma, l'equivalente di quello che sarebbe un void* per un puntatore-a-dato), esattamente come un puntatore-a-funzione è sempre per specifici tipi di argomenti e risultato (non c'è, in C++ standard nè in C standard, nessuna garanzia di potere fare dei cast fra diversi puntatori-a-funzioni, o fra essi e puntatori-a-dati: qualsiasi cast del genere è "undefined behaviour", cioè, un dato compilatore può, come estensione, scegliere di definire questo comportamento, ma non è tenuto a farlo).
Per la grande maggioranza dei problemi che "sembrerebbe" ideale risolvere con "puntatori a metodi di oggetti qualunque", la soluzione ideale sta invece nell'uso di TEMPLATE ("generic programming"); altri si possono risolvere con eredità multipla da classi-interfacce di "mix-in", e RTTI; altri ancora richiedono una rifattorizzazione del proprio progetto.

Torna all'indice


Puntatori a metodi e funzioni statiche membri di classi

C'è una differenza cruciale fra una funzione che è membro (non-statico) di una classe, e una che non lo è. Per il secondo tipo (funzioni "libere", e, indifferentemente, funzioni statiche di una classe) serve solo il puntatore alla funzione stessa, di tipo puntatore-a-funzione; per il primo tipo (funzioni membri non statici di un oggetto), per poterle invocare, servono invece:
  • puntatore all'oggetto su cui invocare
  • member-pointer alla specifica funzione-membro dell'oggetto
Da un punto di vista "astratto", una funzione statica membro di classe non ha stato: non vi è nessuna "informazione ambientale" implicata dalla funzione; è, appunto, una funzione. Un metodo, visto che è sempre associato a un particolare oggetto (non può essere chiamato"in assoluto", ma solo su di un particolare oggetto), ha invece potenzialmente tutto lo stato che l'oggetto può portarsi dietr e inoltre, la identità dell'oggetto stesso (questione distinta da quella dello stato).
Da un punto di vista "concreto", possiamo dire che un metodo ha un "primo argomento speciale, implicito, e nascosto" -- quel puntatore-a-oggetto che entro il metodo stesso si chiama this.
Una funzione-che-non-è-un-metodo (funzione libera, funzione membro statico di una classe) non ha nulla del genere; quindi chiaramente non sono cose "intercambiabili".
Vediamo un esempio specifico.
Supponiamo che la classe base comune di tutti gli oggetti sui quali si può voler invocare dei metodi si chiami Base; dovrà avere come virtuali (tipicamente virtuali puri, ma ciò non è strettamente necessario) tutti i metodi che si può voler invocare. Supponiamo che tutti questi metodi prendano un double e tornino un double. Allora, la struttura "puntatore a metodo e relativo oggetto" sarà simile a:

class Pamero {
  Base* pOggetto;
  typedef double (Base::* pMetodo_t)(double);
  pMetodo_t pMetodo;
 public:
  Pamero(Base *pO, pMetodo_t pM):
  pOggetto(pO), pMetodo(pM){}
  double operator()(double x) {
   return (pOggetto->*pMetodo)(double);
  }
};

Questa classe è scritta in modo che un Pamero sia un funtore,cioè usabile esattamente come useresti un puntatore a funzione. Notiamo inoltre che non abbiamo definito il costruttore di copia, l'assegnamento di copia, e il distruttore, perchè quelli che automaticamente fornisce il compilatore vanno qui benissimo. Pamero non ha invece qui un costruttore di default, il che a volte dà problemi perchè vuol dire che non si può definire un Pamero senza, contestualmente, inizializzarlo; potremmo decidere che il ctor di default faccia un "Pamero nullo", cioè con pOggetto pari a zero, sul quale è vietato chiamare l'operator(), e aggiungere dunque il default ctor

 Pamero(): pOggetto(0) {}

e magari il modo di testare se un Pamero è nullo (meglio evitare le conversioni implicite, e.g. a bool, che danno vari rischi e problemi, e farlo con un metodo esplicito), aggiungendo l'ulteriore metodo inline:

 bool isNull() const { return pOggetto==0; }

Possiamo anche, agevolmente, arricchire Pamero in modo che, in alternativa a chiamare un metodo su di un oggetto di classe Base, possa invece chiamare una funzione non-metodo (libera o statica), sempre con argomento double e valore di ritorno pure double, tanto per esemplificare. Ad esempio:

class Pameroofl {
 Base* pOggetto;
 typedef double (Base::* pMetodo_t)(double);
 typedef double (*pFunz_t)(double);
 union {
  pMetodo_t pMetodo;
  pFunz_t pFunz;
 }
public:
 Pameroofl(pFunz_t pF=0):
 pOggetto(0), pFunz(pF){}
 Pameroofl(Base *pO, pMetodo_t pM):pOggetto(pO), pMetodo(pM){}
 bool isNull() const { return pOggetto==0 && pFunz==0; }
 double operator()(double x) {
  return
  pOggetto?
  (pOggetto->*pMetodo)(double):
  (*pFunz)(double);
 }
};


Questo metodo è perfettamente soddisfacente se si sa con precisione da quale classe (qui, Base) deriveranno gli oggetti su cui interessa poter chiamare i metodi.

Se no, ci sono vari possibili arricchimenti, tipicamente basati sull'uso di template, ed eventualmente anche su metodi virtuali. Vediamo, ad esempio, uno schema del tutto generale. Definiamo anzitutto un'interfaccia astratta ("classe-protocollo" nella terminologia di Lakos):

class Funtore { public:  virtual double operator() double;  // e magari, per comodità...:  double call(double x) { return (*this)(x); } };
[La call serve solo perchè non è comodo chiamare direttamente operatori su puntatori-a-oggetti, e, visto che Funtore è astratta, useremo sempre dei puntatori-a-Funtore, quindi così possiamo scrivere, dato un Funtore* pF,

 return pF->call(x);

invece del più scomodo/goffo

 return (*pF)(x);

o

 return pF->operator()(x);

Comunque, è una banale questione di "zucchero sintattico", di importanza assai minore].

Possiamo chiaramente pensare a varie classi concrete che implementano il "protocollo" Funtore:

class Paf: public Funtore {
 typedef double (*pFunz_t)(double);
 pFunz_t pF;
public:
 Paf(pFunz_t pF=0): pF(pF) {}
 double operator()(double x) { return (*pF)(x); }
};

e

template <class Base> class Pamero: public Funtore {
 Base* pOggetto;
 typedef double (Base::* pMetodo_t)(double);
 pMetodo_t pMetodo;
public:
 Pamero(Base *pO, pMetodo_t pM):pOggetto(pO), pMetodo(pM){}
 double operator()(double x) {
  return (pOggetto->*pMetodo)(double);
 }
};


con le relative "funzioni-fabbrica" in overload/template:

Funtore* newFuntore(double (*pF)(double)) {
 return new Paf(pF);
}

template <class Base> Funtore* newFuntore(Base* pO, double (Base::* pM)(double)) {
 return new Pamero<Base>(pO, pM);
}

(il vantaggio di usare come template delle funzioni è che esse possono dedurre automaticamente il tipo del template sulla base del tipo dei loro argomenti attuali -- molto comodo, in generale, anche se qui, con una sola classe base assai facilmente individuabile, il vantaggio pratico è modesto).

Nota che si può pensare anche a molte altre sottoclassi concrete di Funtore, tutte incapsulabili in modi analoghi a queste; è dunque una soluzione assai generale.

Una classe X che precedentemente teneva un double (*pF)(double) può dunque passare a tenere un Funtore* pF con i soli accorgimenti di settarlo con l'opportuna newFuntore, liberarlo con delete pF quando necessario per evitare memory leak, ecc (per ulteriore comodità si può usare uno smart pointer, o handle, di Funtore, invece del puntatore nudo).

Torna all'indice


Template

Come posso specializzare i template per certi tipi-parametro specifici ?

L'idioma più elegante e generale è quello dei "traits", un template che raggruppa le varie operazioni e caratteristiche di interesse. Ad esempio:

// caso-default
template <typename T>
class comparison_traits {
public:
 static inline bool equal(T t1, T t2) {
  return t1==t2;
 }
// eccetera
};

// casi speciali
template<> class comparison_traits<double> {
public:
 static inline bool equal(double t1, double t2) {
  return fabs(t1-t2) < epsilon;
 }
 // eccetera
};
// da ripetere per float, long double, ecc, o se no
// fare qualche trucchetto di eredita` per risparmiare
// di digitare qualche riga...

Dopo di che, in tutti i template, si può usare

 typedef comparison_traits<T> compare;
 // ...
 if(compare.equal(t1, t2))
  // ecc ecc

o, meglio ancora, prendere compare come un parametro con _default_ comparison_traits<T>, per potere ulteriormente "personalizzare a compile-time" l'esatto comportamento del template.

Torna all'indice


Varie

Quali sono i diversi tipi di cast ?

I "nuovi cast", attualmente raccomandati (il "cast all'antica", ereditato dal C, è deprecato), sono i seguenti:
  • const_cast<tipodiarrivo>(espressione)
    mette e/o toglie "const" (e simili aggettivi, come "volatile")
  • static_cast<tipodiarrivo>(espressione)
    copre la maggior parte delle situazioni: permette di fare qualsiasi trasformazione di tipo che sia implicita, o inversa di una implicita, eccetto per quelle che cura il const_cast (tipi numerici, puntatori parenti fra loro, ecc)
  • reinterpret_cast<tipodiarrivo>(espressione)
    prende i BIT del valore dell'espressione, e, senza nessuna modifica, li interpreta come "tipo di arrivo"; l'uso meno raro è fra puntatori non parenti fra loro (char* e unsigned char* non sono parenti fra loro...)
  • dynamic_cast<tipodiarrivo>(espressione)
    si usa solo fra puntatori o reference a tipi polimorfici; verifica che l'espressione sia effettivamente del tipo desiderato, se no torna 0 (per cast tra puntatori) o dà exception (per cast tra reference)
    dynamic_cast offre una funzionalità del tutto nuova rispetto ai "vecchi cast" (è la "run-time type identification", RTTI). Gli altri, invece, sostanzialmente riducono, e suddividono in modo più preciso, quello che i "vecchi cast" potevano fare (inoltre, offrono una sintassi fatta per "saltare all'occhio", al contrario delle parentesi dei vecchi cast, e tale da rendere facile cercarle con un editor, ecc).

Un costrutto troppo potente, in un linguaggio, va evitato; per questo la potenza eccessiva dei "vecchi cast" viene "ritagliata" (e chiarificata) in tre casi distinti.

  • const_cast va dunque usato in modo esclusivo per mettere/togliere const e simili (solo, naturalmente, se risulta necessario farlo esplicitamente; spesso, ad es passando un char* a una funzione che richiede un const char*, il cast può essere lasciato implicito, "che è meglio"!).

  • static_cast copre tutti quei casi in cui il "nuovo tipo" può richiedere una rappresentazione binaria diversa da quello della espressione (float<->int, puntatori apparentati, ecc)

  • reinterpret_cast, tutti quelli in cui vogliamo che resti invece identica la rappresentazione binaria (ad esempio, se ho un "token opaco" di 32 bit, presentato come un unsigned int, e "so" che è in realtà un char*, oppure un float, oppure..., reinterpret_cast è giusto).

Torna all'indice


Come estrarre un singolo bit da un dato "più grande" ?

Vi sono vari modi, ma il più semplice e tipico è quello di usare l'operatore & (singola ampersand, da leggersi come "AND bit-per-bit" o giù di lì) da applicare mediante una maschera di bit.
Per leggere , ad esempio, il primo bit da un tipo di dato a 32 bit la maschera può essere scritta in vari modi:

 const unsigned int transition_mask = 0x80000000;

oppure

 const unsigned int transition_mask = 1 << 31;

In un tipo di dato a 32 bit qual'è il bit più significativo, il numero 0 o il numero 31 ?
Ogni produttore di chip usa la sua notazione (diversi produttori dello stesso chip a volte usano notazioni diverse fra loro!); Intele Microsoft di solito sono coerenti nel chiamare 0 il meno significativo e così via, così che, se X e` il "numero di bit", 1<<X e` la maschera (decisamente più comodo rispetto alla convenzione contraria).
Per Intel (e MS), la convenzione è coerentemente "little-endian": i bit, byte, ecc, meno significativi, sono sempre quelli identificati da indirizzi, numeri, ecc, numericamente più bassi (beh, al 99+%, almeno -- il 100% non e` di questo mondo! --).
Lo stesso risultato si può ottenere con l'operatore di shift binario a destra >> (o a sinistra <<) ?
Sì, ma attenzione: >> su di un numero CON SEGNO può "propagare" il bit di segno, cioè, se int è 32 bit:

 int pippo = 0x80000000;
 pippo>>31; // puo` essere 0xFFFFFFFF
 unsigned int upippo = 0x80000000;
 upippo>>31; // sicuramente vale 1

meglio quindi, per sapere con certezza cosa succede, applicare lo shift solo a interi SENZA segno.
Comunque, (pippo>>31)&1 dovrebbe essere nuovamente il bit cercato (spostato nella posizione meno significativa), cioè valere 0 o 1.
C'è un MINIMO di overhead (una istruzione di macchina) in questo approccio: la maschera col bit 31 settato è preparata a compile-time (zero costo runtime) quindi l'unica operazione di macchina e` lo AND, mentre questo shift è da farsi a runtime (e uno shift di 31 bit potrebbe prendere molteplici tempi di clock, a seconda di quale sia esattamente la CPU che si sta utilizzando, anche se è espresso come una singola istruzione di macchina). 99+% del tempo questo non ha importanza, ma nel dubbio è meglio prendere l'abitudine di non fare delle elaborazioni "inutili"; cioè, se si deve testare dei bit di una parola, e si sa a priori di che bit si tratta, è meglio farlo con un AND con una maschera costante, piuttosto che con shift E and.
Un altro approccio consiste nell'uso dei "bit-field", ma non lo consiglio -- è meno portabile e quindi più oscuro, per quanto, a prima vista, possa invece sembrare pi6ugrave; leggibile. Comunque, questione di gusti e di stile personale.

Torna all'indice


Come rimuovere la recursione ?

Ecco un esempio: percorrere l'albero dei directory su di un disco, usando le funzioni delle API di Windows FindFirstFile/FindNextFile/FindClose, ma, naturalmente, la struttura della funzione sarebbe identica per qualsiasi altro compito in cui bisogna percorrere (ad esempio per costruirne un modello in memoria) un albero appoggiandosi su funzioni analoghe ("trova il primo figlio del nodo corrente", "trova il prossimo nodo", "Ok, con l'esame dei figli di questo nodo ho finito"). Per concretezza, lo scopo della funzione di esempio è il calcolo dell'occupazione complessiva di disco da parte dell'intero albero, in termini di numero di byte; come albero interessa quello che inizia dal directory corrente, e la specifica del prototipo della funzione è:

 long getOccupiedBytes(void);

Partiamo, anzitutto, dalla versione basata su di una funzione recursiva, basata sull'idea: il numero di byte occupati da un sottoalbero è la sommatoria dei byte dei file che contiene il suo directory-radice, più i byte occupati da ciascun sottoalbero la cui radice è un sottodirectory della radice -- recursivamente. Usiamo inoltre una funzione ausiliaria "bool isDummy(const char* directoryName)" che ritorna true per "." e "..", tipici directory-dummy nei quali non si vuole entrare, banalmente, ad esempio:

 bool isDummy(const char* directoryName) {
  if(0==strcmp(directoryName,".")) return true;
  if(0==strcmp(directoryName,"..")) return true;
  return false;
 }
Infine, ignoriamo per semplicita` il fatto che "un file di 3 byte" non occupa 3 byte, bensi` un intero cluster (solo perchè qui interessa mostrare la struttura...!), come pure i file più grandi di 4 gigabyte, la diagnostica di errori, ecc ecc.

long getOccupiedBytes(void)
{
 long result=0;
 WIN32_FIND_DATA wfd;
 HANDLE hFind = FindFirstFile("*.*",&wfd);

 for(;;) {
  result += wfd.nFileSizeLow;
  if(wfd&FILE_ATTRIBUTE_DIRECTORY) {
   if(!isDummy(wfd.cFileName)) {
    SetCurrentDirectory(wfd.cFileName);
    // la chiamata recursiva:
    result += getOccupiedBytes();
    SetCurrentDirectory("..");
   }
  }
  if(!FindNextFile(hFind,&wfd)) {
   FindClose(hFind);
   return result;
  }
 }
}

Per rimuovere la singola chiamata recursiva, bisogna trovare un altro modo, alternativo alla recursione, di dire, in quel punto: "salva i dati che non vanno alterati, e ricomincia l'elaborazione dall'inizio"; e, dove ora abbiamo il "return, bisogna invece avere un altro modo, compmementare, di dire "recupera i dati che in precedenza erano stati salvati, e riprendi l'esecuzione dal punto del precedente ''ricomincia''" (verificando, naturalmente, se _esistono_ ancora "dati in precedenza salvati"; se no, occorre fare return dall'intera funzione).

Siamo qui molto fortunati, dal punto di vista della semplicità di rimuovere la recursione, perchè c'è un singolo punto di chiamata recursiva, e un singolo punto di ritorno; inoltre, la funzione è priva di argomenti (una bella semplificazione!),E, altra cosa che ci facilita molto, c'è UN SOLO dato da salvare: la HANDLE hFind. Infatti, "result" è usato in modo tale che basta continuare a sommargli i dati, mentre wfd è un "buffer" sovrascritto da ogni chiamata a FindNextFile -- non mantiene dati "importanti" da un ciclo all'altro.

long getOccupiedBytes(void)
{
 long result=0;
 WIN32_FIND_DATA wfd;
 std::vector<HANDLE> savestack;

 for(;;) {
  HANDLE hFind = FindFirstFile("*.*",&wfd);
  for(;;) {
   result += wfd.nFileSizeLow;
   if(wfd&FILE_ATTRIBUTE_DIRECTORY) {
    if(!isDummy(wfd.cFileName)) {
     SetCurrentDirectory(wfd.cFileName);
     savestack.push_back(hFind);
     break; // dal loop interno torna all'esterno
    }
   }
   while(!FindNextFile(hFind,&wfd)) {
    FindClose(hFind);
    SetCurrentDirectory("..");
    if(savestack.empty())
     return result;
    hFind = savestack.back();
    savestack.pop_back();
   }
  }
 }
}

La cosa quasi miracolosa è che qui siamo riusciti a rimuovere la recursione senza nessun esplicito goto...! Di solito (con più di un punto di chiamata recursiva, e/o più di un punto di ritorno), non se ne potrà fare a meno.

Certo, la "amplificazione di complessità" del flusso di controllo della funzione resta pur sempre paurosa, con tre loop annidati: il for(;;) esterno è dove si inizia l'analisi di ogni sub-directory, quello interno il "normale" loop sui file (e sub-dir) di ogni directory, e il while al suo interno (mutazione dell'if della originale routine recursiva) forse il più "trucchistico" di tutti, poichè serve a diagnosticare la fine di un directory e anche a "riprendere" la "normale" analisi del suo directory-padre. Ma forse, a pensarci bene, la cosa più strana è il break del for(;;) interno, che non è la FINE di una qualche sequenza di operazioni, bensi` l'INIZIO di una ricerca "annidata" (infatti, uscendo dal for interno, il controllo passa al for esterno, che riparte con FindFirstFIle).

Normalmente, rimuovere la recursione richiede molte più complicazioni, e, in particolare, rende inevitabile l'uso di espliciti goto (un'alternativa equivalente ad essi è un modello con esplicita "macchina a stati finiti" -- un for(;;) con dentro uno switch() -- che qui lasciamo come esercizio per il lettore).

Le prestazioni della versione non recursiva sono indistinguibili, in termini di misurazioni, da quelle della versione recursiva. Nondimeno, in teoria, un vantaggio ci dovrebbe essere (se le recursioni sono annidate MOLTO profondamente), poichè, nella versione non-recursiva, possiamo (e, in effetti, DOBBIAMO) decidere esplicitamente COSA salvare (qui, la sola HANDLE hFind), mentre una chiamata recursiva "salva tutto" quello che vive nello stack, sia le nostre esplicite variabili, sia dei "dati impliciti" gestiti dal compilatore (frame pointer, instruction pointer alla chiamata per poter eseguire il return "giusto"). In situazioni di memoria estremamente scarsa, e anche di recursioni estremamente profonde, questa unica caratteristica positiva del "recursion removal" può essere proprio quella che "ci salva la ghirba".

Come per tutte le tecniche di ottimizzazione, vale, naturalmente, il motto "impara l'arte, e mettila da parte"; l'ottimizzazione prematura è causa di molti mali (il codice ottimizzato è più difficile da capire, manutenere, modificare, eccetera). In generale sarà più semplice, più produttivo, ed equivalente in termini di prestazioni, scrivere il programma usando dapprima la recursione; se si evidenziano problemi di prestazioni (memoria), allora si può rimboccarsi le maniche e rimuovere la recursione -- lasciando, come commento, la chiara e nitida versione recursiva, ma passando a quella, più complessa e fragile, non-recursiva, per risparmiare qualche prezioso byte, o qualche prezioso ciclo di CPU.

Torna all'indice


Come usare le eccezioni ?

Ecco un esempio:

#include <iostream>

double dividi(double sopra, double sotto)
{
 if(sotto==0.0) throw "Divisione per zero!";
 return sopra/sotto;
}

int main()
{
 try
 {
  double risultato = dividi(1.0,0.0);
  cout << "risultato=" << risultato << "\n";
 }
 catch(const char* p)
 {
  cout << "errore: " << p << "\n";
 }
 return 0;
}

Ovviamente in un esempio-giocattolo l'utilità non traspare molto, ma le exception, usate bene, possono essere preziose in programmi complessi, tutte le volte che una funzione di "basso livello" può scoprire un errore che però non sa come gestire e può solo segnalare ai livelli più alti dell'applicazione -- la funzione in questione farà un throw, un chiamante ad un qualche livello un try/catch.
Per scrivere programmi "solidi" a fronte di eccezioni è importante aderire strettamente al paradigma "l'acquisizione di risorse si fa in un costruttore, il rilascio in un distruttore", ad esempio usando auto_ptr per tenere ogni oggetto allocato dinamicamente -- se avviene un'eccezione, verranno eseguiti i distruttori degli oggetti automatici, ma se un oggetto che si è allocato dinamicamente è tenuto solo da un normale puntatore non lo libera nessuno...
MAI lanciare (nè lasciar propagare) eccezioni da un distruttore, a proposito -- proprio perchè i distruttori sono eseguiti nel corso della gestione di un'eccezione, se uno a sua volta lancia (o lascia propagare) un'eccezione "annidata", bum, viene chiamata la unexpected() e il programma muore piuttosto drammaticamente...

Torna all'indice


A cosa serve namespace ?

Serve a "partizionare" lo spazio dei nomi di un grosso progetto in modo che sottosistemi progettati separatamente non rischino conflitti accidentali di nomi -- problema molto frequente senza i namespace, che porta gli sviluppatori all'uso di appiccicare davanti a tutti i nomi un qualche prefisso... il namespace in pratica istituzionalizza questa pratica e la rende compatibile con la leggibilità del codice.
I namespace sono decisamente pensati per la "programmazione su grande scala".

Torna all'indice


Come definire gli operatori per i tipi user-defined ?

Esempio:

typedef enum
{
 a, b, c, d
} MyEnum;

inline
MyEnum operator++(MyEnum &p)
{
 if(p==d) p = a;
 else p = static_cast<MyEnum>(1+static_cast<int>(p));
 return p;
}

inline
MyEnum operator++(MyEnum& p, int)
{
 MyEnum temp = p;
 ++p;
 return temp;
}

int main()
{
 MyEnum m = a;
 m++; // m vale ora b
 // ecc, ecc
}

Torna all'indice


Perchè non è possibile fare l'overload di funzioni che differiscono solo per il tipo di valore restituito ?

Una "scelta di progetto" del creatore del linguaggio, che, vista la complessità veramente astrusa della risoluzione degli overload in linguaggi che l'ammettevano anche in base al tipo di ritorno, come (in qualche misura) Ada e PL/I, decise di non ammetterla.
La decorazione sulla base del tipo ritornato non è un problema (alcuni compilatori C++ infatti la implementano comunque, per potere aiutare nella diagnosi dell'errore "funzioni che differiscono solo sul tipo di ritorno"); il problema è invece che già oggi le regole di risoluzione di overload sono sin troppo complesse (se non si progettano le cose limitando al minimo assoluto i fattori di complicazione, come i vari operatori di conversione, costruttori a un solo argomento senza explicit, ecc ecc, non è raro ricevere sorprese su quale di tanti overload il compilatore finisca per scegliere applicando precisamente le regole...) -- ammettendo anche l'overload sul tipo di ritorno, le sorprese diverrebbero più frequenti di un ordine di grandezza.

Torna all'indice


Quali sono i diversi tipi di liste e a cosa servono ?

  • Lista invasiva
    (più efficiente, ma si applica solo a lista di classi appositamente progettate per stare in lista!):

    class Impiegato {
     Impiegato* next;
    public:
     // eccetera
    };


  • Lista non-invasiva

    class NodoImpiegato {
     Impiegato* questo;
     NodoImpiegato* next;
    public:
     // eccetera
    };
La lista non-invasiva costa un puntatore in più per nodo (e l'accesso è indiretto, quindi più lento; inoltre, dato solo un impiegato, non si può agevolmente determinare in quali 0 o più liste esso si trovi; ecc); è più flessibile (si può appunto avere un impiegato su 0 o più liste, invece che su esattamente 1). Non sono due usi mutualmente esclusivi, si può avere una lista invasiva per il "contenitore primario" e 0+ liste non invasive degli stessi tipi per vari usi ausiliari.
  • Lista doppiamente linkata
    ogni nodo punta anche al precedente oltre che al successivo, quindi muoversi sulla lista, bidirezionalmente, diventa MOLTO agevole.
Con una lista semplicemente linkata tante operazioni "ovvie" sono disastrose come complicazioni e prestazioni: ad esempio, dato un puntatore a un nodo della lista, come si fa a togliere quel nodo dalla lista? Risposta: si deve partire dall'inizio della lista e trovare il PREDECESSORE di quel nodo, perchè la "rimozione dalla lista" consiste nel settare il Next del predecessore (a quello che ora era il Next del nodo che stiamo eliminando). Giusto per fare un esempio...!
Con una lista doppiamente linkata, togliere un nodo da una lista diventa banalmente facile:

 if(Next) Next->Previous = Previous;
 if(Previous) Previous->Next = Next;

fatto! Le operazioni che sono diabolicamente difficili e mal-prestanti su una lista semplicemente linkata, e banali e velocissime su di una doppiamente linkata, sono TANTE, ma TANTE. Per questo, la lista semplicemente linkata va considerata un "caso particolare", da usare solo a fronte di specifiche e particolari esigenze di uso in condizioni di memoria molto scarsa e preziosa, e cose del genere.

Il C++ Standard fornisce una eccellente implementazione delle liste doppiamente linkate:

 #include <list>
 using std::list;

adesso, per usare una lista di interi, basta che usare list<int>, una lista di oggetti di class Pippo list<Pippo>, una lista di puntatori a tali oggetti list<Pippo*>, e così via, senza limiti.
Si tratta di liste non-invasive (se no non si potrebbe mai avere una list<int>, ad esempio...!). Lo Standard fornisce anche gran copia di algoritmi per operare comodamente con queste (e tante altre) strutture dati.

Torna all'indice


Quali sono i vantaggi delle funzioni inline ?

La keyword inline serve a chiedere al compilatore di non generare una normale chiamata di funzione, ma di inserire il codice della stessa laddove avviene la chiamata.

Es. :

#include <iostream>

inline void Pippo()
{
 std::cout << (1+2);
}

int main()
{
 ...
 Pippo();
 ...
}

Il compilatore trasforma la funzione int main() nella forma :
int main()
{
 ...
 std::cout << (1+2);
 ...
}


Il vantaggio è che si evita l'overhead imposto da una chimata di funzione, ma presenta lo svantaggio di aumentare le dimensioni dell'eseguibile e quindi le funzioni inline vanno utilizzate con attenzione e solo in punti chiave del codice.

Perchè una funzione membro venga espansa inline non è necessario implementarla nella dichiarazione di classe; è possibile eseguire semplicemente una dichiarazione e poi implementarla fuori dalla dichiarazione di classe utilizzando esplicitamente la keyword inline :

inline <ReturnType> <NameClass>::<MemberFunc>( ... )
{
 ...
}


Il vantaggio è quello di rendere più leggibile la dichiarazione di classe (cosa utile per rapide consultazioni dell'header)

Torna all'indice


Come si usano le funzioni con un numero variabile di argomenti ?

Ecco un piccolo esempio :

#include <iostream>
#include <cstdarg>

void numeri(const int n0, ... )
{
 std::va_list args;
 std::va_start(args, n0);
 int n;

 while((n = std::va_arg(args, int))>0)
 {
  std::cout << n << '\n';
 }

 std::cout << std::endl;
 std::va_end(args);
}

int main()
{
 numeri( 0, 1, 2, 3, 4, 5, 10, 20, 0 );
}

Torna all'indice


 



Disclaimer

SourceNet non riconosce nessun tipo di garanzia per il contenuto di tutta la sezione dedicata alle FAQ sul linguaggio C++ pubblicata su questo sito (SourceNet Italia).
Tutto il contenuto della FAQ sul linguaggio C++ è fornito "così come è", senza alcuna garanzia di qualsiasi tipo, sia espressa che implicita, ivi incluse, senza limitazioni, le garanzie implicite di commerciabilità o idoneità per uno scopo particolare ovvero quelle che escludano la violazione di diritti altrui. L'intero rischio derivante dall'uso o dalle prestazioni del contenuto di tutta la sezione della FAQ sul linguaggio C++ rimane a carico dell'utente.


Copyright

Il contenuto della sezione delle FAQ sul linguaggio C++ situata in questo sito è la copia della FAQ sul linguaggio C++, pubblicata all'indirizzo http://www.penguinpowered.com/~The_Cpp_Compass/.
Il contenuto dell'intera FAQ sul linguaggio C++ è di proprietà di Roberto Gerola.
Viene concessa la possibilità di poter copiare il contenuto dell'intera FAQ sul linguaggio C++ di questo sito solo previa autorizzazione di Roberto Gerola (http://www.penguinpowered.com/~The_Cpp_Compass/).


Note sulla revisione

La versione di questa FAQ è aggiornata alla FAQ sul linguaggio C++, ultimo aggiornamento 24/08/1999, di Roberto Gerola (http://www.penguinpowered.com/~The_Cpp_Compass/).

 

Torna ad inizio pagina

Le FAQ sul linguaggio C++ - Pagina 2 ] [ Le FAQ sul linguaggio C++ - Pagina 3 ]

Ultimo aggiornamento : 17/01/2009.   
Home ] Su ] Novità ] FAQ ] Hardware ] Software ] Windows ] Sicurezza ] Linguaggi ] Cerca ] Contatta ] Guestbook ] Mappa ] Info su... ]

Copyright © 1997-2070, Joseph Parrello. Tutti i diritti sono riservati.

Siete il visitatore n. Contatore Sito
Bpath Contatore
dal 17 gennaio 2009.