The C++ Compass

The C++ Compass
>FAQ<

FAQ

Home
Home

Risorse
Risorse

Utilities
Utilities

Compilatori
Compilatori

GUI Toolkits
GUI Toolkits

Libri
Libri

Download
Download

FAQ
FAQ



whoami
Who am I
[Precedente] [Indice] [Successiva]


[Libreria standard e containers]


[Puntatori]


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).




Ultimo aggiornamento : 03/07/2000

email webmaster@thecppcompass.org