MindView Inc.

 

[ Suggerimenti ] [ Soluzioni degli Esercizi] [ Volume 2 ] [ Newsletter Gratuita ]
[
Seminari ] [ Seminari su CD ROM ] [ Consulenza]

Pensare in C++, seconda ed. Volume 1

©2000 by Bruce Eckel

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]

traduzione italiana e adattamento a cura di Marco Arena

9: Funzioni Inline

Una delle caratteristiche importanti di C++ ereditata dal C è l'efficienza. Se l'efficienza del C++ è drasticamente minore del C, ci può essere una folta rappresentanza di programmatori che non può giustificare il suo uso.

In C, uno dei modi di preservare l'efficienza è attraverso l'uso di macro, queste permettono di fare ciò che appare come una chiamata ad una funzione ma senza l'overhead tipico della chiamata. La macro è implementata con il pre-processore invece che dalla stesso processore, e il pre-processore sostituisce tutte le chiamate alle macro direttamente con il codice delle macro, quindi non c'è costo derivante dal pushing degli argomenti, esecuzione di una CALL del linguaggio assembler, il ritorno di argomenti e l'esecuzione di un RETURN di linguaggio assembler. Tutto il lavoro è eseguito dal pre-processore, così si ha la convenienza e la leggibilità di una chiamata a funzione senza costi aggiuntivi.

Ci sono due problemi con l'uso delle macro in C++. Il primo già cruciale in C: una macro assomiglia a una chiamata di funzione, ma non sempre agisce come tale. Questo può "nascondere" errori difficili da trovare. Il secondo problema è specifico di C++: il pre-processore non ha il permesso di accedere a dati delle classi membro. Questo significa che le macro non possono essere usate come funzioni di classi membro.

Per mantenere l'efficienza delle macro, ma aggiungere la sicurezza e lo scope di classe di una vera funzione,il C++ offre le funzioni inline. In questo capitolo, tratteremo dei problemi delle macro in C++, di come questi siano stati risolti con le funzioni inline, delle linee guida e delle intuizioni del modo di lavorare inline.

Le insidie del pre-processore

Il punto chiave dei problemi delle macro è che ci si può ingannare nel pensare che il comportamento del pre-processore è lo stesso del compilatore. Naturalmente è stato spiegato che una macro è simile e lavora come una chiamata a funzione, per cui è facile cadere in quest' equivoco. Le difficoltà iniziano quando si affrontano le sottili differenze.

Come semplice esempio, si consideri il seguente:

#define F (x) (x + 1)

Ora, se viene fatta una chiamata F come questa

F(1)

il preprocessore la sviluppa, piuttosto inaspettatamente, nella seguente:

(x) (x + 1)(1)

Il problema sorge a causa della distanza tra F e la sua parentesi aperta nella definizione della macro. Quando questo spazio viene rimosso, si può effettivamente chiamare la macro con lo spazio

F (1)

ed essa sarà sviluppata correttamente in

(1 + 1)

L'esempio precedente è abbastanza insignificante e il problema si renderà evidente subito. Le difficoltà reali nasceranno quando saranno usate espressioni come argomenti nelle chiamate a macro.

Ci sono due problemi. Il primo e che le espressioni possono essere sviluppate all'interno delle macro per cui l'ordine di valutazione è differente da ciò che normalmente ci si aspetta. Per esempio,

#define FLOOR(x,b) x>=b?0:1

Ora, se vengono usate espressioni per gli argomenti

if(FLOOR(a&0x0f,0x07)) // ...

la macro sarà sviluppata come

if(a&0x0f>=0x07?0:1)

La precedenza di & è minore rispetto a quella di >=, quindi la valutazione della macro ci sorprenderà. Una volta scoperto il problema, lo si può risolvere mettendo parentesi intorno ad ogni cosa nella definizione della macro (questa è una buona regola da usare quando si creano macro). Quindi,

#define FLOOR(x,b) ((x)>=(b)?0:1)

Scoprire il problema può essere difficile, comunque, e si può non trovarlo finché non si è dato per scontato l'esatto comportamento della macro. Nella versione senza parentesi della precedente macro, molte espressioni lavoreranno correttamente perché la precedenza di >= è minore di molti operatori come +, /, , e anche degli operatori di shift su bit. Quindi si può facilmente iniziare a pensare che essa lavori con tutte le espressioni, incluse quelle che usano gli operatori logici su bit.

Il problema precedente può essere risolto con un'attenta pratica di programmazione: mettere le parentesi a tutto nelle macro. Tuttavia, la seconda difficoltà è più insidiosa. Diversamente da una funzione normale, ogni volta che si usa un argomento in una macro, questo viene valutato. Finché la macro è chiamata solo con variabili ordinarie, questa valutazione non avra effetti negativi, ma se la valutazione di un argomento ha effetti collaterali, allora i risultati possono essere sorprendenti e il comportamento non seguirà affatto quello di una funzione.

Per esempio, questa macro determina se il suo argomento cade all'interno di un certo intervallo:

#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)

Finché si usa un argomento "ordinario", la macro lavorerà praticamente come una funzione reale. Ma non appena si diventa meno attenti pensando che sia una funzione reale, cominciano i problemi. Quindi:

//: C09:MacroSideEffects.cpp
#include "../require.h"
#include <fstream>
using namespace std;

#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)

int main() {
  ofstream out("macro.out");
  assure(out, "macro.out");
  for(int i = 4; i < 11; i++) {
    int a = i;
    out << "a = " << a << endl << '\t';
    out << "BAND(++a)=" << BAND(++a) << endl;
    out << "\t a = " << a << endl;
  }
} ///:~

Da notare l'uso di caratteri maiuscoli per il nome della macro. Questa è una regola utile perché dice al lettore che essa è una macro e non una funzione, così se ci sono problemi o dubbi ciò funziona da piccolo promemoria.

Ecco l'output prodotto dal programma, che non è niente affatto ciò che ci si sarebbe aspettato da una vera funzione:

a = 4
  BAND(++a)=0
   a = 5
a = 5
  BAND(++a)=8
   a = 8
a = 6
  BAND(++a)=9
   a = 9
a = 7
  BAND(++a)=10
   a = 10
a = 8
  BAND(++a)=0
   a = 10
a = 9
  BAND(++a)=0
   a = 11
a = 10
  BAND(++a)=0
   a = 12

Quando a vale quattro, solo la prima parte della condizione basta (perché già non rispettata), quindi l'espressione viene valutata una volta e l'effetto collaterale della chiamata a macro è che a diventa cinque, il che è ciò che ci si aspetta da una normale chiamata a funzione nella stessa situazione. Tuttavia, quando il numero cade all'interno del range, verranno testate entrambe le condizioni dell'if il che comporta due incrementi. Il risultato viene prodotto valutando di nuovo l'argomento, per cui si passa a un terzo incremento. Una volta che il numero esce fuori dal range, entrambe le condizioni sono ancora testate avendo ancora due incrementi. Gli effetti collaterali sono differenti, dipendono dall'argomento.

Questo non è chiaramente il tipo di comportamento che si vuole da una macro che assomigli a una funzione. In questo caso, la soluzione ovvia è renderla una vera funzione, il che naturalmente aggiunge un overhead extra e può ridurre l'efficienza se si chiama spesso la funzione. Sfortunatamente, il problema può non essere sempre così ovvio, si può, ad esempio, avere una libreria che a nostra insaputa, contenga funzioni e macro mischiate insieme, quindi un problema come questo può nascondere bug molto difficili da trovare. Per esempio, la macro putc( ) nella libreria cstdio può valutare il suo secondo argomento due volte. Questo è specificato nello Standard C. Inoltre anche le implementazioni poco attente di toupper( ) come macro possono valutare l'argomento più di una volta, il che può dare risultati inaspettati con toupper(*p++).[45]

Macro e accessi

Naturalmente, in C è richiesta una codifica accurata e l'uso di macro, potremmo certamente ottenere la stessa cosa in C++ se non ci fosse un problema: una macro non ha il concetto di scope richiesto con le funzioni membro. Il preprocessore semplicemente esegue una sostituzione di testo, quindi non si può dire qualcosa tipo

class X {
  int i;
public:
#define VAL(X::i) // Errore

o qualcosa simile. Inoltre , può non esserci indicazione a quale oggetto ci si stia riferendo. Semplicemente non c'è modo di esprimere lo scope di classe in una macro. Senza alcuna alternativa per le macro, i programmatori sarebbero tentati di rendere alcuni dati public per amore dell'efficienza, svelando così l'implementazione sottostante e impedendo cambiamenti in quell'implementazione, così come l'eliminazione della protezione fornita dalla direttiva private.

Funzioni Inline

Risolvendo in C++ il problema delle macro con accesso a classi membro di tipo private, verranno eliminati tutti i problemi associati alle macro. Ciò è stato fatto portando il concetto di macro sotto il controllo del compilatore dove esse appartengono. Il C++ implementa le macro come funzioni inline, che è una vera e propria funzione in ogni senso. Qualsiasi comportamento ci si aspetti da una funzione ordinaria, lo si otterrà da una funzione inline. La sola differenza e che una f.i. è sviluppata sul posto, come una macro, così da eliminare l'overhead della chiamata a funzione. Pertanto, si possono (quasi) mai usare le macro, ma solo le funzioni inline.

Ogni funzione definita all'interno del corpo di una classe è automaticamente inline, ma è possibile rendere una funzione inline facendo precedere la definizione di funzione dalla parola riservata inline. Comunque, affinché si abbia un qualche effetto si deve includere il corpo della funzione con la dichiarazione, altrimenti il compilatore la tratterà come una funzione ordinaria. Pertanto,

inline int plusOne(int x);

non ha effetto su tutto ma solo sulla dichiarazione della funzione (che in seguito può essere o meno una funzione inline). L'approccio giusto prevede invece:

inline int plusOne(int x) { return ++x; }

Da notare che il compilatore verificherà (come sempre) l'esattezza della lista degli argomenti della funzione e del valore di ritorno (eseguendo eventuali conversioni), cose che il preprocessore è incapace di fare. Inoltre provando a scrivere il codice precedente come una macro, si otterrà un effetto indesiderato.

Quasi sempre le funzioni inline saranno definite in un file header. Quando il compilatore vede una tale definizione, mette il tipo di funzione (il nome insieme al valore di ritorno) e il corpo della funzione stessa nella sua tabella dei simboli. Quando la funzione sarà richiamata, il compilatore verificherà l'esattezza della chiamata e l'uso corretto del valore di ritorno, e sostituisce il corpo della funzione con la chiamata stessa, eliminando così l'overhead. Il codice inline occupa spazio, ma se la funzione è piccola, ciò effettivamente prende meno spazio del codice generato per fare una chiamata a una funzione ordinaria (con il pushing degli argomenti nello stack e l'esecuzione della CALL).

Una funzione inline in un file header ha uno stato speciale, si deve includere il file header contenente la funzione e la sua definizione in ogni file dove la funzione è usata, ma non si finisce il discorso con la definizione multipla di errori (comunque, la definizione deve essere identica in tutti i posti dove la funzione inline è inclusa).

Inline all'interno delle classi

Per definire una funzione inline, si deve normalmente precedere la definizione di funzione con la parola riservata inline. Comunque ciò non è necessario all'interno delle definizioni di classi. Ogni funzione definita all'interno di una classe è automaticamente inline. Per esempio:

//: C09:Inline.cpp
// L'inline all'interno delle classi
#include <iostream>
#include <string>
using namespace std;

class Point {
  int i, j, k;
public:
  Point(): i(0), j(0), k(0) {}
  Point(int ii, int jj, int kk)
    : i(ii), j(jj), k(kk) {}
  void print(const string& msg = "") const {
    if(msg.size() != 0) cout << msg << endl;
    cout << "i = " << i << ", "
         << "j = " << j << ", "
         << "k = " << k << endl;
  }
};

int main() {
  Point p, q(1,2,3);
  p.print("value of p");
  q.print("value of q");
} ///:~

Qui, i due costruttori e la funzione print( ) sono tutte inline per default. Da notare che nel main( ) il fatto che si stiano usando funzioni inline è del tutto trasparente, come è giusto che sia. Il comportamento logico di una funzione deve essere identico al di là del fatto che essa sia o meno inline (altrimenti il compilatore è rotto). La sola differenza si noterà nelle prestazioni.

Naturalmente, la tentazione è di usare le funzioni inline ovunque all'interno delle dichiarazioni di classe perchè si risparmia lo step extra di creare una definizione di funzione esterna. Ricordarsi che la tecnica inline serve per fornire buone opportunità per l'ottimizzazione del compilatore . Ma rendere inline una funzione grande causerà una duplicazione di codice ovunque la funzione venga chiamata, gonfiando il codice e diminuendo la velocità (il solo modo sicuro per scoprirlo è sperimentare agli effetti di rendere inline un programma sul proprio compilatore).

Access functions (funzioni d'accesso)

Uno degli usi più importanti della tecnica inline all'interno delle classi è l'access function. Questa è una piccola funzione che permette di leggere o cambiare parte dello stato di un'oggetto - cioè variabili o una variabile interna. Il motivo per cui la tecnica inline è importante per le access functions può essere visto nel seguente esempio:

//: C09:Access.cpp
// Inline  e access functions

class Access {
  int i;
public:
  int read() const { return i; }
  void set(int ii) { i = ii; }
};

int main() {
  Access A;
  A.set(100);
  int x = A.read();
} ///:~

Qui, l'utente della classe non ha mai un contatto diretto con lo stato della variabile all'interno della classe ed esse possono essere mantenute private, sotto il controllo del progettista della classe. Tutti gli accessi a dati di tipo private può essere controllato attraverso la funzione interfaccia. In più, l'accesso è notevolmente efficiente. Si consideri il read( ), per esempio. Senza l'inline, il codice generato per la chiamata a read( ) dovrà tipicamente includere il pushing (inserimento) nello stack e fare una chiamata assembler CALL. Con molte macchine, la dimensione di questo codice potrà essere più grande della dimensione del codice creato dalla tecnica inline, e il tempo d''esecuzione potrà essere certamente più lungo.

Senza le funzioni inline, un progettista che privilegia l'efficienza sarà tentato di dichiarare semplicemente i di tipo public, eliminando l'overhead permettendo all'utente di accedere direttamente ad i. Da un punto di vista progettuale, ciò è disastroso perchè i in tal caso diverrebbe parte dell'interfaccia pubblica, il che significa che chi ha scritto la classe non può più cambiarla. Si rimane bloccati con un int chiamato i. Questo è un problema perchè ci si potrebbe accorgere prima o poi che può essere più utile rappresentare quell'informazione come float piuttosto che con un int, ma perchè int i è parte di un interfaccia pubblica, non la si può più cambiare. Magari si potrebbe voler eseguire calcoli supplementari oltre a leggere o settare i, ma non si può se questo è public. Se, d'altronde, si sono sempre usate funzioni per leggere o cambiare lo stato di un'oggetto, si può modificare la rappresentazione sottostante dell'oggetto (per la gioia del cuore..).

Inoltre, l'uso di funzioni per controllare dati permette di aggiungere codice alla funzione per capire quando il valore del dato viene cambiato, il che può essere molto utile durante il debugging. Se una dato è public, chiunque può cambiarlo in qualunque momento.

Accessors e mutators

Qualcuno preferisce dividere il concetto delle access functions in accessors (leggere lo stato delle informazioni da un oggetto, che accede) e mutators (cambiare lo stato di un oggetto, che muta). Inoltre l'overloading delle funzioni può essere usato per fornire lo stesso nome di funzione sia per l'accessor che per il mutator; il modo in cui si chiama la funzione determina se si sta leggendo o modificando lo stato dell'informazione. Ad esempio,

//: C09:Rectangle.cpp
// Accessors e mutators

class Rectangle {
  int wide, high;
public:
  Rectangle(int w = 0, int h = 0)
    : wide(w), high(h) {}
  int width() const { return wide; } // Legge
  void width(int w) { wide = w; } // Setta
  int height() const { return high; } // Legge
  void height(int h) { high = h; } // Setta
};

int main() {
  Rectangle r(19, 47);
  // Cambia width & height:
  r.height(2 * r.width());
  r.width(2 * r.height());
} ///:~

Il costruttore usa la lista di inizializzazione (introdotta nel capitolo 8 e trattata completamente nel capitolo 14) per inizializzare i valori di wide e high (usando la forma dello pseudo costruttore per i tipi predefiniti).

Non si possono avere nomi di funzioni e usare lo stesso identificatore come dato membro, per cui si sarebbe tentati di distinguere i dati con un trattino di sottolineatura. Comunque, gli identificatori con il trattino di sottolineatura sono riservati e non si possono usare.

Si può scegliere invece di usare -get- e -set- per indicare accessor e mutator:

//: C09:Rectangle2.cpp
// Accessors e mutators con "get" e "set"

class Rectangle {
  int width, height;
public:
  Rectangle(int w = 0, int h = 0)
    : width(w), height(h) {}
  int getWidth() const { return width; }
  void setWidth(int w) { width = w; }
  int getHeight() const { return height; }
  void setHeight(int h) { height = h; }
};

int main() {
  Rectangle r(19, 47);
  // Cambia  width & height:
  r.setHeight(2 * r.getWidth());
  r.setWidth(2 * r.getHeight());
} ///:~

Naturalmente, accessors e mutators non devono essere semplici pipeline per una variabile interna. Qualche volta possono eseguire calcoli più complicati. L'esempio seguente usa le funzioni della libreria Standard C per produrre una semplice classeTime :

//: C09:Cpptime.h
// Una semplice classe time
#ifndef CPPTIME_H
#define CPPTIME_H
#include <ctime>
#include <cstring>

class Time {
  std::time_t t;
  std::tm local;
  char asciiRep[26];
  unsigned char lflag, aflag;
  void updateLocal() {
    if(!lflag) {
      local = *std::localtime(&t);
      lflag++;
    }
  }
  void updateAscii() {
    if(!aflag) {
      updateLocal();
      std::strcpy(asciiRep,std::asctime(&local));
      aflag++;
    }
  }
public:
  Time() { mark(); }
  void mark() {
    lflag = aflag = 0;
    std::time(&t);
  }
  const char* ascii() {
    updateAscii();
    return asciiRep;
  }
  // Differenza in secondi:
  int delta(Time* dt) const {
    return int(std::difftime(t, dt->t));
  }
  int daylightSavings() {
    updateLocal();
    return local.tm_isdst;
  }
  int dayOfYear() { // Dal 1° Gennaio
    updateLocal();
    return local.tm_yday;
  }
  int dayOfWeek() { // Da Domenica
    updateLocal();
    return local.tm_wday;
  }
  int since1900() { // Anni dal 1900
    updateLocal();
    return local.tm_year;
  }
  int month() { // Da Gennaio
    updateLocal();
    return local.tm_mon;
  }
  int dayOfMonth() {
    updateLocal();
    return local.tm_mday;
  }
  int hour() { // Dalla mezzanotte, orario 24-ore
    updateLocal();
    return local.tm_hour;
  }
  int minute() {
    updateLocal();
    return local.tm_min;
  }
  int second() {
    updateLocal();
    return local.tm_sec;
  }
};
#endif // CPPTIME_H ///:~

Le funzioni della libreria Standard di C hanno diverse rappresentazioni per il tempo, e queste fanno tutte parte della classe Time. Comunque, non è necessario aggiornarle tutte, per tanto time_t t è usata come rappresentazione base, tm local e asciiRep (rappresentazione dei caratteri ASCII) hanno ciascuno dei flag per indicare se devono essere aggiornate al valore corrente di time_t. Le due funzioni private, updateLocal( ) e updateAscii( ) verificano i flags e di conseguenza eseguono l'aggiornamento.

Il costruttore chiama la funzione mark( ) (che può essere anche chiamata dall'utente per forzare l'oggetto a rappresentare il tempo corrente) e questo azzera i due flags per indicare che l'ora locale e la rappresentazione ASCII non sono più valide. La funzione ascii( ) chiama updateAscii( ), la quale copia il risultato della funzione della libreria standard asctime( ) nel buffer locale perchè asctime( ) usa un'area dati statica che viene sovrascritta ogni volta che si chiama. Il valore di ritorno della funzione ascii( ) è l'indirizzo di questo buffer locale.

Tutte le funzioni che iniziano con daylightSavings( ) usano la funzione updateLocal( ), la quale provoca come conseguenza per la struttura inline di essere abbastanza pesante. Questo non deve sembrare utile, specialmente considerando che probabilmente non si vorrà chiamare la funzione molte volte. Comunque, questo non deve significare che tutte le funzione devono essere fatte non-inline. Se si vogliono tutte le altre funzioni non-inline, conviene almeno mantenere updateLocal( ) inline, in tal modo il suo codice sarà duplicato nelle funzioni non-inline, eliminando l'overehead extra.

Ecco un piccolo programma test:

//: C09:Cpptime.cpp
// Test di una semplice classe time
#include "Cpptime.h"
#include <iostream>
using namespace std;

int main() {
  Time start;
  for(int i = 1; i < 1000; i++) {
    cout << i << ' ';
    if(i%10 == 0) cout << endl;
  }
  Time end;
  cout << endl;
  cout << "start = " << start.ascii();
  cout << "end = " << end.ascii();
  cout << "delta = " << end.delta(&start);
} ///:~

Un oggetto Time viene creato, poi vengono eseguite alcune attività mangia-tempo e dopo viene creato un secondo oggetto Time per segnare il tempo finale. Questi vengono usati per mostrare il tempo iniziale, finale e trascorso.

Stash e Stack con l'inline

Armati della tecnica inline, si possono adesso convertire le classi Stash e Stack per una maggiore efficienza:

//: C09:Stash4.h
// Inline functions
#ifndef STASH4_H
#define STASH4_H
#include "../require.h"

class Stash {
  int size;      // Dimensione di ogni spazio
  int quantity;  // Numero di spazi per lo storage
  int next;      // Prossimo spazio libero
  // Array di bytes allocati dinamicamente:
  unsigned char* storage;
  void inflate(int increase);
public:
  Stash(int sz) : size(sz), quantity(0),
    next(0), storage(0) {}
  Stash(int sz, int initQuantity) : size(sz), 
    quantity(0), next(0), storage(0) { 
    inflate(initQuantity); 
  }
  Stash::~Stash() {
    if(storage != 0) 
      delete []storage;
  }
  int add(void* element);
  void* fetch(int index) const {
    require(0 <= index, "Stash::fetch (-)index");
    if(index >= next)
      return 0; // Per indicare la fine
   // Produce un puntatore all'elemento desiderato:
    return &(storage[index * size]);
  }
  int count() const { return next; }
};
#endif // STASH4_H ///:~

Le funzioni piccole ovviamente lavorano bene con la tecnica inline, ma da notare che le due funzioni grandi sono ancora lasciate come non-inline, usando anche per loro la tecnica inline non si avrebbe un guadagno nelle performance:

//: C09:Stash4.cpp {O}
#include "Stash4.h"
#include <iostream>
#include <cassert>
using namespace std;
const int increment = 100;

int Stash::add(void* element) {
  if(next >= quantity) // Abbastanza spazio rimasto?
    inflate(increment);
  // Copia l'elemento nello storage,
  // parte dal prossimo spazio libero:
  int startBytes = next * size;
  unsigned char* e = (unsigned char*)element;
  for(int i = 0; i < size; i++)
    storage[startBytes + i] = e[i];
  next++;
  return(next - 1); // Indice
}

void Stash::inflate(int increase) {
  assert(increase >= 0);
  if(increase == 0) return;
  int newQuantity = quantity + increase;
  int newBytes = newQuantity * size;
  int oldBytes = quantity * size;
  unsigned char* b = new unsigned char[newBytes];
  for(int i = 0; i < oldBytes; i++)
    b[i] = storage[i]; // Copia il vecchio sul nuovo
  delete [](storage); // Rilascia lo storage vecchio
  storage = b; // Punta alla nuova memoria
  quantity = newQuantity; // Aggiusta la dimensione
} ///:~

Ancora una volta, il programma verifica che tutto funzioni correttamente:

//: C09:Stash4Test.cpp
//{L} Stash4
#include "Stash4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;

int main() {
  Stash intStash(sizeof(int));
  for(int i = 0; i < 100; i++)
    intStash.add(&i);
  for(int j = 0; j < intStash.count(); j++)
    cout << "intStash.fetch(" << j << ") = "
         << *(int*)intStash.fetch(j)
         << endl;
  const int bufsize = 80;
  Stash stringStash(sizeof(char) * bufsize, 100);
  ifstream in("Stash4Test.cpp");
  assure(in, "Stash4Test.cpp");
  string line;
  while(getline(in, line))
    stringStash.add((char*)line.c_str());
  int k = 0;
  char* cp;
  while((cp = (char*)stringStash.fetch(k++))!=0)
    cout << "stringStash.fetch(" << k << ") = "
         << cp << endl;
} ///:~

Questo è lo stesso programma-test usato prima, per cui l'output deve essere sostanzialmente lo stesso.

La classe Stack fa perfino miglior uso della tecnica inline:

//: C09:Stack4.h
// Con l'inline
#ifndef STACK4_H
#define STACK4_H
#include "../require.h"

class Stack {
  struct Link {
    void* data;
    Link* next;
    Link(void* dat, Link* nxt): 
      data(dat), next(nxt) {}
  }* head;
public:
  Stack() : head(0) {}
  ~Stack() {
    require(head == 0, "Stack not empty");
  }
  void push(void* dat) {
    head = new Link(dat, head);
  }
  void* peek() const { 
    return head ? head->data : 0;
  }
  void* pop() {
    if(head == 0) return 0;
    void* result = head->data;
    Link* oldHead = head;
    head = head->next;
    delete oldHead;
    return result;
  }
};
#endif // STACK4_H ///:~

Da notare che il distruttore Link, che era presente ma vuoto nella precedente versione di Stack è stato rimosso. In pop( ), l'espressione delete oldHead semplicemente libera la memoria usata da Link (ciò non distrugge il dato puntato daLink).

La maggior parte delle funzioni diventa inline piuttosto esattamente e ovviamente, specialmente per Link. Perfino pop( ) sembra lecito, sebbene qualche volta si possono avere condizioni o variabili locali per le quali non è chiaro che la tecnica inline sia la più utile. Qui la funzione è piccola abbastanza tanto che probabilmente non danneggia alcunché.

Se tutte le funzioni sono rese inline, l'uso della libreria diventa abbastanza semplice perchè non c'e necessità del linking, come si può vedere nell'esempio (da notare che non c'è Stack4.cpp):

//: C09:Stack4Test.cpp
//{T} Stack4Test.cpp
#include "Stack4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;

int main(int argc, char* argv[]) {
  requireArgs(argc, 1); // L'argomento è il nome del file
  ifstream in(argv[1]);
  assure(in, argv[1]);
  Stack textlines;
  string line;
  // Legge il file e memorizza le linee nello stack:
  while(getline(in, line))
    textlines.push(new string(line));
  // Estrae le linee dallo stack e le stampa:
  string* s;
  while((s = (string*)textlines.pop()) != 0) {
    cout << *s << endl;
    delete s; 
  }
} ///:~

C'è chi scriverà, in qualche caso, classi con tutte funzioni inline, così che l'intera classe sarà in un file header. Durante lo sviluppo di un programma ciò è probabilmente innocuo, sebbene in qualche caso può rendere più lunga la compilazione. Ma una volta che il programma si stabilizza, si può tornare indietro e scrivere funzioni non-inline dove è possibile.

L'inline e il compilatore

Per capire dove la tecnica inline è efficace, è utile conoscere cosa fa il compilatore quando incontra una funzione inline. Come per ogni funzione, il compilatore, memorizza il tipo della funzione (cioè, il prototipo della funzione includendo il nome e i tipi degli argomenti, insieme al valore di ritorno della funzione) nella sua tabella dei simboli. In più, quando il compilatore vede che il tipo della funzione inline e il corpo della funzione sono analizzabili senza errori, anche il codice per la funzione viene tirato dentro la tabella dei simboli. In qualsiasi forma sia memorizzato il codice sorgente, istruzioni assembler compilate o altre rapresentazioni spetta al compilatore deciderlo.

Quando si fa una chiamata ad una funzione inline, il compilatore per prima cosa si assicura che la chiamata può essere fatta in modo corretto. Ovvero tutti i tipi degli argomenti devono o essere giusti nella lista degli argomenti o il compilatore deve essere in grado di fare una conversione di tipo verso i tipi esatti ed inoltre il valore di ritorno deve essere di tipo corretto (o convertibile) nell'espressione di destinazione. Questo, naturalmente, è esattamente ciò che il compilatore fa per ogni funzione ed è notevolmente diverso da ciò che fa il preprocessore, che non può far verifiche sui tipi o eseguire conversioni.

Se tutte le informazioni sul tipo di funzione si adattano con il contesto della chiamata, il codice inline viene sostituito direttamente alla chiamata di funzione, eliminando l'overhead di chiamata e permettendo ulteriori ottimizzazioni al compilatore. Inoltre, se il codice inline è una funzione membro, l'indirizzo dell'oggetto (this) viene inserito al posto giusto, il che, ovviamente, è un'altra azione che il preprocessore non è in grado di fare.

Limitazioni

Ci sono due situazioni nelle quali il compilatore non può eseguire l'inlining. In questi casi, semplicemente, ritorna alla forma ordinaria di una funzione prendendo la definizione inline e memorizzandola proprio come una funzione non-inline. Se deve fare questo in unità di conversioni multiple (le quali normalmente causano un errore di definizione multipla), il linker è in grado di ignorare le definizioni multiple.

Il compilatore non può eseguire l'inlining se la funzione è troppo complicata. Questo dipende dal particolare compilatore, ma su questo punto molti compilatori rinunciano, la tecnica inline quindi non apporterà probabilmente un aumento di efficienza. In generale ogni sorta di loop è considerata troppo complicata da espandere con la tecnica inline e, pensandoci sopra, un ciclo probabilmente comporta molto più tempo all'interno della funzione che non quello richiesto dall'overhead di chiamata. Se la funzione è solo un insieme di semplici istruzioni, il compilatore non avrà probabilmente difficoltà nell'applicare l'inlining, ma se ci sono molte istruzioni, l'overhead della chiamata sarà minore del costo di esecuzione del corpo della funzione. Si ricordi che ogni volta che si chiama una grossa funzione inline, l'intero corpo della funzione viene inserito al posto della chiamata, per cui facilmente si ottiene un "rigonfiamento" del codice senza apprezzabili miglioramenti delle prestazioni (da notare che alcuni esempi di questo libro possono eccedere le dimensioni ragionevoli per la tecnica inline in favore della salvaguardia delle proprietà dello schermo).

Il compilatore inoltre non può eseguire l'inlining se l'indirizzo della funzione è preso implicito o esplicito. Se il compilatore deve produrre un indirizzo, allora esso allocherà memoria per il codice della funzione e userà l'indirizzo derivato. Comunque, quando un indirizzo non è richiesto, il compilatore probabilmente applicherà la tecnica inline al codice.

E' importante capire che la tecnica inline è solo una proposta al compilatore; quest'ultimo non è forzato a fare niente inline. Un buon compilatore applicherà la tecnica inline con funzioni piccole e semplici mentre intelligentemente ignorerà tale tecnica per quelle troppo complicate. Ciò darà i risultati sperati: l'esatta semantica della chiamata a funzione con l'efficienza di una macro.

Riferimenti in avanti

Se si immagina cosa faccia il compilatore per implementare la tecnica inline, ci si può confondere nel pensare che ci siano più limitazioni di quante ne esistano effettivamente. In particolare, se una funzione inline fa un riferimento in avanti ad un'altra funzione che non è stata ancora dichiarata nella classe (al di là del fatto che sia inline o meno), può sembrare che il compilatore non sia in grado di maneggiarlo:

//: C09:EvaluationOrder.cpp
// Ordine di valutazione dell'inline

class Forward {
  int i;
public:
  Forward() : i(0) {}
  // Chiamata a funzioni non dichiarate:
  int f() const { return g() + 1; }
  int g() const { return i; }
};

int main() {
  Forward frwd;
  frwd.f();
} ///:~

In f( ), viene fatta una chiamata a g( ), sebbene g( ) non è stata ancora dichiarata. Ciò funziona perchè il linguaggio stabilisce che le funzioni non-inline in una classe saranno valutate fino alla parentesi graffa di chiusura della dichiarazione di classe.

Naturalmente, se g( ) a sua volta chiama f( ), si avrebbero una serie di chiamate ricorsive che sarebbero troppo complicate per il compilatore da gestire con l'inline ( inoltre, si dovrebbero eseguire alcune prove in f( ) or g( ) per forzare una di esse a "raggiungere il livello più basso", altrimenti la ricorsione sarebbe infinita).

Attività nascoste nei costruttori e distruttori

Costruttori e distruttori sono due situazioni in cui si può pensare che l'inline è più efficiente di quanto non lo sia realmente. Costruttori e distruttori possono avere attività nascoste, perchè la classe può contenere suboggetti dai quali costruttori e distruttori devono essere chiamati. Questi suboggetti possono essere normali oggetti o possono esistere a causa dell'ereditarietà (trattata nel Capitolo 14). Come esempio di classe con oggetti membro:

//: C09:Hidden.cpp
// Attività nascoste nell'inline
#include <iostream>
using namespace std;

class Member {
  int i, j, k;
public:
  Member(int x = 0) : i(x), j(x), k(x) {}
  ~Member() { cout << "~Member" << endl; }
};

class WithMembers {
  Member q, r, s; // costruttori
  int i;
public:
  WithMembers(int ii) : i(ii) {} // Insignificante?
  ~WithMembers() {
    cout << "~WithMembers" << endl;
  }
};

int main() {
  WithMembers wm(1);
} ///:~

Il costruttore per Member è abbastanza semplice da trattare con la tecnica inline poichè non c'e niente di speciale da fare - nessuna eredità o oggetti membro che causano attività nascoste. Ma nella classe WithMembers c'e molto di più di cui occuparsi di quanto salta all'occhio. I costruttori e distruttori per gli oggetti q, r, e s sono stati chiamati automaticamente, e quei costruttori e distruttori sono pure inline, per cui la differenza da una normale funzione è significativa. Questo non deve necessariamente significare che si dovrebbero sempre fare definizioni di costruttori e distruttori non-inline; ci sono casi in cui ciò ha senso. Inoltre, quando si sta facendo una "bozza" iniziale di un programma per scrivere velocemente il codice, è spesso molto conveniente usare la tecnica inline. Ma se si ricerca l'efficienza, è una situazione da osservare .

Ridurre la confusione

In un libro come questo, la semplicità e concisione di mettere le definizioni inline all'interno delle classi è molto utile perché ben si adattano su una pagina o su un video (come in un seminario). Comunque, Dan Saks[46] ha posto in rilievo che in un progetto reale ciò ha l'effetto di ingombrare inutilmente l'interfaccia della classe e quindi rendere la classe molto pesante da usare. Egli ricorre a funzioni membro definite dentro le classi, usando il latino in situ (sul posto), e sostiene che tutte le definizioni dovrebbero essere piazzate fuori dalla classe per mantenere l'interfaccia pulita. L'ottimizzazione che egli intende dimostrare è una questione separata. Se si vuole ottimizzare il codice, si usi la parola riservata inline.Usando questo approccio, l'esempio di prima, Rectangle.cpp diventa:

//: C09:Noinsitu.cpp
// Rimuovere le funzioni in situ 

class Rectangle {
  int width, height;
public:
  Rectangle(int w = 0, int h = 0);
  int getWidth() const;
  void setWidth(int w);
  int getHeight() const;
  void setHeight(int h);
};

inline Rectangle::Rectangle(int w, int h)
  : width(w), height(h) {}

inline int Rectangle::getWidth() const {
  return width;
}

inline void Rectangle::setWidth(int w) {
  width = w;
}

inline int Rectangle::getHeight() const {
  return height;
}

inline void Rectangle::setHeight(int h) {
  height = h;
}

int main() {
  Rectangle r(19, 47);
  // Transpone Width e Height:
  int iHeight = r.getHeight();
  r.setHeight(r.getWidth());
  r.setWidth(iHeight);
} ///:~

Ora se si vuole paragonare l'effetto delle funzioni inline con quelle non-inline, semplicemente si può rimuovere la keyword inline (le funzioni inline dovrebbero trovarsi normalmente nei file header, per quanto possibile, mentre le funzioni non-inline devono trovarsi nella propria unità di conversione). Se si vogliono mettere le funzioni nella documentazione, lo si può fare con una semplice operazione di taglia-e-incolla. Le funzioni in situ necessitano di maggior lavoro e potenzialmente possono presentare più errori. Un'altra controversia per questo approccio è che si può sempre produrre uno stile di formattazione coerente per le definizioni di funzioni, qualcosa che non sempre è necessario con le funzioni in situ.

Ulteriori caratteristiche del preprocessore

In precedenza, ho detto che quasi sempre si vogliono usare le funzioni inline invece delle macro. Le eccezioni sorgono quando si ha bisogno di usare tre caratteristiche speciali del preprocessore C (che poi è anche il preprocessore C++): stringizing (convertire in stringhe ndt), concatenazione di stringhe e token pasting (incollatura di identificatori). Stringizing, introdotta in precedenza nel libro, viene eseguita con la direttiva # e permette di prendere un identificatore e convertirlo in una stringa. La concatenazione di stringhe si ha quando due stringhe adiacenti non hanno caratteri di punteggiatura tra di loro, nel qual caso esse vengono unite. Queste due caratteristiche sono particolarmente utili nello scrivere codice di debug. Cioè,

#define DEBUG(x) cout << #x " = " << x << endl

Questo stampa il valore di ogni variabile. Si può anche ottenere una traccia che stampi le istruzioni eseguite:

#define TRACE(s) cerr << #s << endl; s

La direttiva #s converte in stringa le istruzioni per l'output e la seconda s reitera l'istruzione così esso viene eseguito. Naturalmente, questo tipo di cosa può causare problemi, specialmente con un ciclo for di una sola linea:

for(int i = 0; i < 100; i++)
 TRACE(f(i));

Poichè ci sono esattamente due istruzioni nella macro TRACE( ), il ciclo for di una linea, esegue solo la prima. La soluzione è di sostituire il punto e virgola con una virgola nella macro.

Token pasting

Token pasting, implementato con la direttiva ##, è molto utile quando si sta scrivendo il codice. Essa permette di prendere due identificatori e incollarli insieme per creare automaticamente un nuovo identificatore. Per esempio,

#define FIELD(a) char* a##_string; int a##_size
class Record {
  FIELD(one);
  FIELD(two);
  FIELD(three);
  // ...
}; 

Ogni chiamata alla macro FIELD( ) crea un identificatore per memorizzare una stringa e un altro per memorizzare la lunghezza di questa. Non solo è di facile lettura, ma può eliminare errori di codice e rendere la manutenzione più facile.

Miglioramenti nell'error checking

Le funzioni require.h sono state usate fino a questo punto senza definirle (sebbene assert( ) è già stata usata per aiutare a trovare gli errori di programmazione dove appropriato). E' il momento di definire questo file header. Le funzioni inline qui sono convenienti perchè permettono ad ogni cosa di essere posizionata in un file header, il che semplifica il processo di utilizzo dei package. Basta includere il file header senza il bisogno di preoccuparsi del linking di file.

Si dovrebbe notare che le eccezioni (presentate in dettaglio nel Volume 2 di questo libro) forniscono un modo molto più efficace di maneggiare molte specie di errori – specialmente quelli che si vogliono riparare– invece di fermare il programma. Le condizioni che tratta require.h, comunque, sono quelle che impediscono la continuazione del programma, in modo simile a quando l'utente non fornisce sufficienti argomenti alla riga di comando o quando un file non può essere aperto. Perciò, è accettabile la chiamata alla funzione exit() della libreria Standard C.

Il seguente file header si trova nella root directory del libro, per cui facilmente accessibile da tutti i capitoli.

//: :require.h
// Test per le condizioni di errore nei programmi
// Per i primi compilatori inserire "using namespace std"
#ifndef REQUIRE_H
#define REQUIRE_H
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>

inline void require(bool requirement, 
  const std::string& msg = "Richiesta fallita"){
  using namespace std;
  if (!requirement) {
    fputs(msg.c_str(), stderr);
    fputs("\n", stderr);
    exit(1);
  }
}

inline void requireArgs(int argc, int args, 
  const std::string& msg = 
    "Devi usare %d argomenti") {
  using namespace std;
   if (argc != args + 1) {
     fprintf(stderr, msg.c_str(), args);
     fputs("\n", stderr);
     exit(1);
   }
}

inline void requireMinArgs(int argc, int minArgs,
  const std::string& msg =
    "Devi usare almeno  %d argomenti") {
  using namespace std;
  if(argc < minArgs + 1) {
    fprintf(stderr, msg.c_str(), minArgs);
    fputs("\n", stderr);
    exit(1);
  }
}
  
inline void assure(std::ifstream& in, 
  const std::string& filename = "") {
  using namespace std;
  if(!in) {
    fprintf(stderr, "Impossibile aprire il file %s\n",
      filename.c_str());
    exit(1);
  }
}

inline void assure(std::ofstream& out, 
  const std::string& filename = "") {
  using namespace std;
  if(!out) {
    fprintf(stderr, "Impossibile aprire il file %s\n", 
      filename.c_str());
    exit(1);
  }
}
#endif // REQUIRE_H ///:~

I valori di default forniscono messaggi ragionevoli che possono essere cambiati se necessario.

Si noterà che invece di usare argomenti char*, vengono usati const string&. Ciò permette per queste funzioni argomenti sia char* che string, e quindi in generale molto più utile (si può voler seguire questo modello nel proprio codice).

Nelle definizioni di requireArgs( ) e requireMinArgs( ), viene aggiunto 1 al numero di argomenti necessari sulla linea di comando perchè argc già include il nome del programma che viene eseguito come argomento 0, e quindi già ha un valore che è uno in più del numero degli argomenti presenti sulla linea di comando.

Si noti l'uso delle dichiarazioni locali “using namespace std” dentro ogni funzione. Ciò perchè alcuni compilatori nel momento della scrittura di questo libro non includevano erroneamente le funzioni standard della libreria C in namespace std, per cui un uso esplicito potrebbe causare un errore a compile-time. La dichiarazione locale permette a require.h di lavorare sia con librerie corrette che con quelle incomplete evitando la creazione di namespace std per chiunque includa questo file header.

Ecco un semplice programma per testare require.h:

//: C09:ErrTest.cpp
//{T} ErrTest.cpp
// Test di  require.h
#include "../require.h"
#include <fstream>
using namespace std;

int main(int argc, char* argv[]) {
  int i = 1;
  require(i, "value must be nonzero");
  requireArgs(argc, 1);
  requireMinArgs(argc, 1);
  ifstream in(argv[1]);
  assure(in, argv[1]); // Use il nome del file
  ifstream nofile("nofile.xxx");
  // Fallimento:
//!  assure(nofile); // L'argomento di default
  ofstream out("tmp.txt");
  assure(out);
} ///:~

Si potrebbe essere tentati di fare un passo ulteriore per aprire file e aggiungere macro a require.h:

#define IFOPEN(VAR, NAME) \
  ifstream VAR(NAME); \
  assure(VAR, NAME);

Che potrebbero essere usate così:

IFOPEN(in, argv[1])

Dapprima, questo potrebbe sembrare interessante poichè sembra ci sia da digitare di meno. Non è terribilmente insicuro, ma è una strada che è meglio evitare. Si noti come, ancora una volta, una macro appare come una funzione ma si comporta diversamente; essa effettivamente crea un oggetto (in) il cui scope dura al di là della la macro. Si può capire questo, ma per i nuovi programmatori e i manutentori di codice è solo una cosa in più da decifrare. Il C++ è già complicato abbastanza di per sè, per cui è bene evitare di usare le macro ogni qualvolta si può.

Sommario

E' di importanza cruciale essere abili a nascondere l'implementazione sottostante di una classe perchè si può voler cambiare questa in seguito. Si faranno questi cambiamenti per aumentare l'efficienza, o perchè si arriva a una migliore comprensione del problema, o perchè si rendono disponibili nuove classi che si vogliono usare nell'implementazione. Qualsiasi cosa che metta in pericolo la privacy dell'implementazione riduce la flessibilità del linguaggio. Per questo, la funzione inline è molto importante perchè essa virtualmente elimina il bisogno delle macro e i loro problemi correlati. Con la tecnica inline, le funzioni possono essere efficienti come macro.

Le funzioni inline possono essere usate nelle definizioni di classi, naturalmente. Il programmatore è tentato di fare così perchè è più facile, e così avviene. Comunque, non è che un punto di discussione, infatti più tardi, cercando un'ottimizzazione delle dimensioni, si possono sempre cambiare le funzioni in funzioni non-inline senza nessuno effetto sulla loro funzionalità. La linea guida dello sviluppo del codice dovrebbe essere “Prima rendilo funzionante, poi ottimizzalo.

Esercizi

Le soluzioni agli esercizi proposti può essere trovata nel documento elettronico The Thinking in C++ Annotated Solution Guide, disponibile a un costo accessibile, all'indirizzo www.BruceEckel.com.

  1. Scrivere un programma che usi la macro F( ) vista all'inizio del capitolo e dimostrare che essa non si sviluppa correttamente, come descritto nel testo. Riparare la macro e mostrare che essa opera correttamente.
  2. Scrivere un programma che usi la macro FLOOR( ) vista all'inizio del capitolo. Mostrare sotto quali condizioni essa non opera correttamente.
  3. Modificare l'esempio MacroSideEffects.cpp in modo che BAND( ) operi correttamente.
  4. Creare due funzioni identiche, f1( ) e f2( ). Rendere inline f1( ) e lasciare f2( ) come funzione non-inline. Usare la funzione clock( ) della libreria Standard C che si trova in <ctime> per segnare il punto di inizio/fine e paragonare le due funzioni per vedere quale è più veloce. Si può aver bisogno di fare ripetute chiamate alle funzioni all'interno di un ciclo per ottenere numeri utili.
  5. Provare, variando la dimensioni e complessità del codice dentro le funzioni dell'esercizio 4, a vedere se si trova un punto di equilibrio, in termini di tempi di esecuzione, tra funzione inline e non-inline. Se si ha la possibilità, provare questo esercizio con differenti compilatori e valutare le differenze.
  6. Provare che le funzioni inline mancano di un linkaggio interno.
  7. Creare una classe che contenga un array di char. Inserire un costruttore inline che usi la funzione della libreria Standard C memset( ) per inizializzare l'array all'argomento del costruttore (valore di default per questo ‘ ’), e una funzione inline chiamata print( ) per stampare tutti i caratteri dell'array.
  8. Considerare l'esempio NestFriend.cpp del Capitolo 5 e sostituire tutte le funzioni normali con funzioni inline. Renderle funzioni inline non-in situ. Cambiare inoltre la funzione initialize( ) in costruttore.
  9. Modificare StringStack.cpp del Capitolo 8 usando funzioni inline.
  10. Creare un tipo enum chiamato Hue contenente red, blue,e yellow. Creare poi una classe chiamata Color contenente una variabile di tipo Hue e un costruttore che setti Hue al suo argomento. Aggiungere access functions per “get” e “set” Hue. Scrivere tutte le funzioni inline.
  11. Modificare l'esercizio 10 per usare l'approccio “accessor” e “mutator”.
  12. Modificare Cpptime.cpp in modo che esso misuri il tempo dal momento in cui il programma inizia l'esecuzione al momento in cui un utente premi il tasto “Enter” o “Return” .
  13. Creare una classe con due funzioni inline, tale che la prima che è definita nella classe chiami la seconda funzione, senza il bisogno di dichiarazioni in avanti. Scrivere un main che crei un oggetto della classe e chiami la prima funzione.
  14. Creare una classe A con un costruttore inline che presenti se stesso. Fare poi una nuova classe B e mettere un oggetto di A come membroB, e dare a B un costruttore inline. Creare un array di oggetti B e vedere cosa succede.
  15. Creare una grande quantità di oggetti dal precedente esercizio, e usare la classe Time per cronometrare la differenza tra costruttori non-inline e inline. .
  16. Scrivere un programma che accetti una stringa come argomento alla linea comando. Scrivere un ciclo for che rimuova una carattere dalla stringa a ogni passo, e usare la macro DEBUG( ) di questo capitolo per stampare la stringa ogni volta.
  17. Correggere la macro TRACE( ) macro come specificata in questo capitolo, a provare che questa lavori correttamente.
  18. Modificare la macro FIELD( ) in modo che essa contenga anche un indice. Creare una classe i cui membri sono composti di chiamate alla macro FIELD( ). Aggiungere una funzione che permetta di cercare un campo usando l'indice. Scrivere un main( ) per testare la classe.
  19. Modificare la macro FIELD( ) in modo che essa automaticamente generi access function per ogni campo ( i dati dovrebbero essere private, comunque). Creare una classe i cui membri sono composti di chiamate alla macro FIELD( ). Scrivere un main( ) per testare la classe.
  20. Scrivere un programma che accetti due argomenti alla linea di comando: il primo è un int e il secondo è un nome di file. Usare require.h per accertarsi che si ha il giusto numero di argomenti, che l'intero è compreso tra 5 e 10, e che il file può essere aperto con successo.
  21. Scrivere un programma che usi la macro IFOPEN( ) per aprire un file come input stream. Giustificare la creazione dell'oggetto ifstream e il suo scopo.
  22. (Da sfida) Determinare come fa il proprio compilatore a generare codice assembly. Creare un file contenente una funzione molto piccola ed un main( ) che chiami la funzione. Generare il codice assembly quando la funzione è inline e non-inline, e dimostrare che la versione inline non ha overhead per chiamata a funzione.


[45]Andrew Koenig entra in maggiori dettagli nel suo libro C Traps & Pitfalls (Addison-Wesley, 1989).

[46] Co-autore con Tom Plum di C++ Programming Guidelines, Plum Hall, 1991.

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
Ultimo Aggiornamento:24/12/2002