04
Dec
2007

Introducere

O intrebare frecvent intalnita este "Cum pot afla/verifica in C++ tipul unui obiect la run-time?".

Sa luam o problema simpla:
Completati codul de mai jos in asa fel incat la primul apel functia CFoo::SpuneCevaMaiAnimalule sa afiseze "Ham! " iar la al doilea sa afiseze "Miau! ". Cu alte cuvinte, sa se verifice la run-time daca am transmis ca parametru un pointer la un obiect de tipul Caine sau la Pisica.

class Animal {/*...*/}; 
class Caine : public Animal {/*...*/}; 
class Pisica : public Animal {/*...*/}; 
class CFoo 
{ 
public: 
   void SpuneCevaMaiAnimalule(Animal*) {/*...*/} 
}; 

int main(int argc, char* argv[]) 
{ 
   Caine  grivei; 
   Pisica mitzi; 
   CFoo   foo; 

   foo.SpuneCevaMaiAnimalule(&grivei); 
   foo.SpuneCevaMaiAnimalule(&mitzi); 

   return 0; 
}

O prima incercare

Prima idee ar fi sa adaugam un membru care sa tina informatia despre tip.

#include <iostream> 

class Animal 
{ 
public: 
   enum TipAnimal {TipCaine, TipPisica}; 
}; 

class Caine : public Animal 
{ 
public: 
   Caine() : m_tipAnimal(TipCaine) {} 
   const TipAnimal m_tipAnimal; 
}; 

class Pisica : public Animal 
{ 
public: 
   Pisica() : m_tipAnimal(TipPisica) {} 
   const TipAnimal m_tipAnimal; 
}; 

class CFoo 
{ 
public: 
   void SpuneCevaMaiAnimalule(Animal*); 
}; 

int main(int argc, char* argv[])
{
   // ... (vezi codul din enuntul problemei)
}

void CFoo::SpuneCevaMaiAnimalule(Animal* pAnimal) 
{ 
   if(((Caine*)pAnimal)->m_tipAnimal == Animal::TipCaine) 
      std::cout << "Ham! "; 
   else if(((Pisica*)pAnimal)->m_tipAnimal == Animal::TipPisica) 
      std::cout << "Miau! "; 
}

Priviti, va rog, la implementarea lui CFoo::SpuneCevaMaiAnimalule si ganditi-va ca am avea nu doua ci treizeci si noua de tipuri (clase derivate din Animal). Urata rau! Greu de scris, greu de inteles, usor de facut greseli. Desi se putea si mai rau...

...se poate si mai bine

Hai sa inbunatatim putin solutia!
Tin numele clasei intr-un membru static privat de tip string si il accesez printr-o functie virtuala.

#include <iostream> 
#include <string> 

class Animal 
{ 
public: 
   virtual bool EsteDeTipul(const std::string&) = 0; 
}; 

class Caine : public Animal 
{ 
   static const string m_nume_clasa; 
public: 
   virtual bool EsteDeTipul(const std::string&); 
}; 

class Pisica : public Animal 
{ 
   static const string m_nume_clasa; 
public: 
   virtual bool EsteDeTipul(const std::string&); 
}; 

class CFoo 
{ 
public: 
   void SpuneCevaMaiAnimalule(Animal*); 
}; 

int main(int argc, char* argv[]) 
{ 
   // ... (vezi codul din enuntul problemei) 
} 

const string  Caine::m_nume_clasa  = "Caine"; 
const string  Pisica::m_nume_clasa = "Pisica";

bool Caine::EsteDeTipul(const std::string& nume_clasa) 
{ 
   return (nume_clasa == m_nume_clasa); 
} 

bool Pisica::EsteDeTipul(const std::string& nume_clasa) 
{ 
   return (nume_clasa == m_nume_clasa); 
} 

void CFoo::SpuneCevaMaiAnimalule(Animal* pAnimal) 
{ 
   if(pAnimal->EsteDeTipul("Caine")) 
      std::cout << "Ham! "; 
   else if(pAnimal->EsteDeTipul("Pisica")) 
      std::cout << "Miau! "; 
}

CFoo::SpuneCevaMaiAnimalule pare acum ceva mai clara si mai usor de scris.
Putem merge mai departe prin definirea urmatoarelor macro-uri...

#include <iostream> 
#include <string>

#define DECLARE_RUNTIME_CLASS \
private: static const std::string m_nume_clasa; \
public: virtual bool EsteDeTipul(const std::string&); 

#define IMPLEMENT_RUNTIME_CLASS(nume_clasa) \
const std::string nume_clasa##::m_nume_clasa = #nume_clasa; \
bool nume_clasa##::EsteDeTipul(const std::string& nume) \
{return (nume == m_nume_clasa);}

// ...

...ramanand astfel si mai putin cod de scris:

// ...
class Animal 
{ 
public: 
   virtual bool EsteDeTipul(const std::string&) = 0; 
}; 

class Caine : public Animal 
{ 
   DECLARE_RUNTIME_CLASS 
}; 

class Pisica : public Animal 
{ 
   DECLARE_RUNTIME_CLASS 
}; 

class CFoo 
{ 
public: 
   void SpuneCevaMaiAnimalule(Animal*); 
}; 

int main(int argc, char* argv[]) 
{ 
   // ... (vezi codul din enuntul problemei) 
} 

IMPLEMENT_RUNTIME_CLASS(Caine) 
IMPLEMENT_RUNTIME_CLASS(Pisica) 

void CFoo::SpuneCevaMaiAnimalule(Animal* pAnimal) 
{ 
   if(pAnimal->EsteDeTipul("Caine")) 
      std::cout << "Ham! "; 
   else if(pAnimal->EsteDeTipul("Pisica")) 
      std::cout << "Miau! "; 
}

In felul acesta ne-am apropiat de solutia din MFC ( Microsoft Foundation Classes library).

Daca folosim MFC, atunci e "floare la ureche"

Majoritatea claselor din MFC deriva din CObject care printre altele implementeaza si un mecanism de acces al tipului la run-time. Este un pic mai complicat decat in exemplul de mai sus dar tebuie sa-l iertam pentru ca face mai multe decat ne dea pur si simplu tipul obiectului. Mai multe despre "minunatele proprietati" ale clasei CObject pot face subiectul unui articol separat. Oricum, daca folosim MFC, problema noastra se poate rezolva simplu si elegant, cam asa:

#include <afx.h> 

class Animal : public CObject 
{ 
   DECLARE_DYNAMIC(Animal) 
}; 

class Caine : public Animal 
{ 
   DECLARE_DYNAMIC(Caine) 
}; 

class Pisica : public Animal 
{ 
   DECLARE_DYNAMIC(Pisica) 
}; 

class CFoo 
{ 
public: 
   void SpuneCevaMaiAnimalule(Animal*); 
}; 

int main(int argc, char* argv[]) 
{ 
   // ... (vezi codul din enuntul problemei) 
} 

IMPLEMENT_DYNAMIC(Animal, CObject) 
IMPLEMENT_DYNAMIC(Caine, Animal) 
IMPLEMENT_DYNAMIC(Pisica, Animal) 

void CFoo::SpuneCevaMaiAnimalule(Animal* pAnimal) 
{ 
   if(pAnimal->IsKindOf(RUNTIME_CLASS(Caine))) 
      printf("Ham! "); 
   else if(pAnimal->IsKindOf(RUNTIME_CLASS(Pisica))) 
      printf("Miau! "); 
}

O alta abordare: RTTI

O alta abordare este folosirea mecanismul RTTI ( Run-Time Type Information) al limbajului C++. Acesta permite folosirea a doi operatori "speciali": typeid si dynamic_cast.

Prima rezolvare cu RTTI a problemei noastre este prin folosirea operatorului typeid. Acesta intoarce o referinta la un type_info care tine informatii despre tipul obiectului pasat ca parametru.

#include <iostream> 

class Animal {virtual void Dummy(){};}; 
class Caine : public Animal{}; 
class Pisica : public Animal{}; 
class CFoo 
{ 
public: 
   void SpuneCevaMaiAnimalule(Animal*); 
}; 

int main(int argc, char* argv[]) 
{ 
   // ... (vezi codul din enuntul problemei)
} 

void CFoo::SpuneCevaMaiAnimalule(Animal* pAnimal) 
{ 
   const type_info& ti = typeid(*pAnimal); 

   if(ti == typeid(Caine)) 
      std::cout << "Ham! "; 
   else if(ti == typeid(Pisica)) 
      std::cout << "Miau! "; 
}

Frumos!
Poate ati observat ca in clasa Animal am pus o functie virtuala Dummy, desi aparent nu foloseste la nimic. De ce? Simplu: ca sa mearga jucarica cu RTTI-ul, trebuie o clasa de tip "polimorfic", adica sa aiba cel putin o functie virtuala.
In plus, nu uitati sa faceti "enable" la RTTI din setarile (proprietatile) proiectului.

Putem de asemenea sa ne bazam pe operatorul dynamic_cast. Acesta intoarce zero daca ii plasam un pointer la un obiect de tipul care nu trebuie.
Deci am putea scrie

void CFoo::SpuneCevaMaiAnimalule(Animal* pAnimal) 
{ 
   if(dynamic_cast<Caine*>(pAnimal)) 
      std::cout << "Ham! "; 
   else if(dynamic_cast<Pisica*>(pAnimal)) 
      std::cout << "Miau! "; 
}

La urma urmei la ce bun?

Acestea fiind zise, am si eu o intrebare: chiar e nevoie sa stim tipul unui obiect la run-time? Eu zic ca nu. Putem uita toate chestiile de mai sus cu conditia sa ne amintim de polimorfism si sa intelegem pentru ce, de fapt, a bagat nenea Stroustrup functiile virtuale in C++.

#include <iostream> 

class Animal 
{ 
public: 
   virtual void IacaSpun() = 0; 
}; 
class Caine : public Animal 
{ 
public: 
   virtual void IacaSpun(); 
}; 
class Pisica : public Animal 
{ 
public: 
   virtual void IacaSpun(); 
}; 
class CFoo 
{ 
public: 
   void SpuneCevaMaiAnimalule(Animal*);
}; 

int main(int argc, char* argv[]) 
{ 
   // ... (vezi codul din enuntul problemei) 
} 

void Caine::IacaSpun() 
{ 
   std::cout << "Ham! "; 
} 
void Pisica::IacaSpun() 
{ 
   std::cout << "Miau! "; 
} 
void CFoo::SpuneCevaMaiAnimalule(Animal* pAnimal) 
{ 
   pAnimal->IacaSpun(); 
}

O ultima privire la CFoo::SpuneCevaMaiAnimalule. Frumoasa, curata, simpla, nu-i asa?

The neverending story

Inchei, tot cu cu o mica problema:
Completati codul urmator (aceeiasi problema ca mai sus, numai ca facem dynamic_cast la o referinta si nu la un pointer).

void CFoo::SpuneCevaMaiAnimalule(Animal& rAnimal) 
{ 
   dynamic_cast<Caine&>(rAnimal); 
   dynamic_cast<Pisica&>(rAnimal); 
}