piątek, 21 maja 2010

typeof w szablonach C++

Często kiedy wykorzystujemy szablony w C++ pojawia się pytanie - jak sprawdzić w czasie kompilacji, czy typ T jest podtypem typu U?

Kolejna wersja standardu C++ wprowadzi operator typeof. Jest on już zaimplementowany w pewnych kompilatorach (np. g++). Można wykorzystać operator typeid z C++ lub dynamic_cast, ale to działa w czasie wykonania, przez co zwalnia wykonanie programu.

Ale jak się okazuje, typeof czasu kompilacji można zaimplementować w standardowym C++ już teraz. Ja np. jeszcze w grudniu twierdziłem, że to niemożliwe :)

Rozwiązanie tkwi w operatorze sizeof. Oblicza on rozmiar dowolnego wyrażenia wykorzystując tylko informacje o typach dostępne w czasie kompilacji, co pozwoli nam z użyciem paru sztuczek napisać odpowiednik typeof ;)

Koncepcja jest taka:

Chcemy sprawdzić, czy typ T jest typu U. T jest typu U, kiedy można naturalnie zrzutować T na U - tak naturalnie, że kompilator robi to za nas (automatyczna konwersja). Czyli typ T jest podtypem U lub jest to ten sam typ.

Weźmy więc przeciążoną funkcję - jedno przeciążenie będzie przyjmować typ U, drugie "cokolwiek innego". Przeciążenia będą miały różny typ zwracany, a te typy będą się różnić rozmiarami. Zbadamy je z użyciem operatora sizeof i sprawa załatwiona :)

Wg standardu sizeof(char) == 1, więc mamy pierwszego kandydata. Typy takie jak int czy long mają prawdopodobnie inny rozmiar, ale standard C++ tego nie gwarantuje. Ale gwarantuje on, że klasa z tablicą 2 elementów typu char ma rozmiar większy od 1 ;)

typedef char Small;
class Big { char dummy[2]; };


Teraz potrzebujemy dwóch przeciążeń. Trzeba się zastanowić chwilę nad tym przyjmującym "cokolwiek innego" - musi to być przypadek automatycznej konwersji, który zachodzi tylko wtedy, jak już nic lepszego nie ma ;) Trzy kropki to właśnie takie coś:

Small Test(U);
Big Test(...);


Nie piszemy implementacji tych funkcji, ponieważ operator sizeof opiera się tylko na deklaracjach.

Teraz potrzebujemy argumentu - czyli wartości typu T. Można by spróbować wykorzystać konstruktor domyślny T(), ale przy braku takiej konstrukcji pojawiłby się błąd kompilacji. Dlatego zastosujemy kolejną sztuczkę, pamiętając, że operator sizeof nie wymaga implementacji wyrażeń, których rozmiar oblicza - wartość T uzyskamy z funkcji, która zwraca taki typ:

T MakeT();

Mamy wszystkie składniki, aby napisać klasę do badania konwersji:

template <class T, class U>
class Conversion
{
  typedef char Small;
  class Big { char dummy[2]; };
  static Small Test(U);
  static Big Test(...);
  static T MakeT();
public:
  enum { exists = sizeof(Test(MakeT())) == sizeof(Small) };
};


OK, ale przydałaby nam się jeszcze możliwość stwierdzenia, iż typ T i U to ten sam typ. Do klasy Conversion dopisujemy więc jeszcze jedno wyliczenie:

enum { sameType = false };

Czyli w ogólnym przypadku T i U to nie to samo. Ale tworzymy specjalizację naszej klasy dla przypadku, kiedy typ jest ten sam - i tam zmieniamy wartość wyliczenia:

template <class T>
class Conversion<T, T>
{
public:
  enum { exists = true };
  enum { sameType = true };
};


Teraz mamy wszystko czego potrzebujemy, aby napisać własny operator TYPEOF:

#define TYPEOF(T, U) \
(Conversion<const T*, const U*>::exists && \
!Conversion<const U*, const void*>::sameType)


TYPEOF(U, T) zwróci true wtedy i tylko wtedy, kiedy T jest podtypem U lub typ samym typem.

Spytacie po co dodatkowy test z const void*? Test jest wykonywany na wskaźnikach typu const, aby ten modyfikator nie psuł niczego. Jednak wtedy dowolne T byłoby typu void, ponieważ istnieje automatyczna konwersja const T* na const void*. Przypadek ten eliminowany jest przez drugi test.


Na koniec jeszcze raz całość kodu:

template <class T, class U>
class Conversion
{
  typedef char Small;
  class Big { char dummy[2]; };
  static Small Test(U);
  static Big Test(...);
  static T MakeT();
public:
  enum { exists = sizeof(Test(MakeT())) == sizeof(Small) };
  enum { sameType = false };
};

template <class T>
class Conversion<T, T>
{
public:
  enum { exists = true };
  enum { sameType = true };
};

#define TYPEOF(T, U) \
(Conversion<const U*, const T*>::exists && \
!Conversion<const T*, const void*>::sameType)


Więcej magii C++ w książce Andreia Alexandrescu "Nowoczesne projektowanie w C++", którą posłużyłem się przy tym poście :)

2 komentarze:

  1. Od kiedy w c++ jest coś takiego jak ... ? bo chyba nie jestem na bieżąco

    OdpowiedzUsuń
  2. :) Jest to pozostałość po C, która umożliwia przekazywanie zmiennej liczby argumentów do funkcji. Najpopularniejszą funkcją, która to wykorzystuje jest printf ;)

    Wielokropka jednak lepiej nie używać, jak się samemu pisze funkcje ;)

    Więcej:

    http://www.learncpp.com/cpp-tutorial/714-ellipses-and-why-to-avoid-them/

    OdpowiedzUsuń