2011-03-29

C++ 0x - decltype

Nowe słowo kluczowe zdefiniowane w standardzie C++ 0x. Zostało zaimplementowane w Visual Studio 2010.

Służy do wnioskowania typu na podstawie wyrażenia. Przykłady:

// && zachowuje się tutaj podobnie jak & albo *
const int&& fx()
{
    return 5; 
}

class A 
{ 
    public:

        double x; 
        static double sx;

        double xx() const
        {
            return 5;
        }

        static double sxx()
        {
            return 5;
        }
};

void test1()
{
    // zadeklarujmy trochę zmiennych.
    int i1 = 3;
    const int i2 = 5;
    double f1 = 5;
    const double f2 = 6;
    const A* a = new A();

    //
    // decltype z zmiennej to typ zmiennej
    //

    decltype(i1) x41 = i1; // int
    x41 = i1;
    decltype(i1) x42 = i2; // int
    x42 = i2;

    decltype(i2) x21 = i1; // const int
    //x21 = i1; błąd
    decltype(i2) x22 = i2; // const int
    //x22 = i2; błąd

    // decltype z wywołania funkcji to rezultat tej funkcji
    decltype(fx()) x11 = fx(); // const int&&, funkcja nie 
                               // zostanie wywołana, typ ustalany
                               // jest podczas kompilacji.
    const int&& x12 = fx();

    // decltype z wskaźnika na funkcje to wskaźnik na funkcje.
    decltype(fx) x2; // const int&&()
    const int&& (*x3)() = fx;

    decltype(a->x) x33 = f1; // double
    x33 = f1;

    decltype(a->x) x34 = f2; // double
    x34 = f2;

    decltype((a->x)) x35 = f1; // const double&
    //x35 = f1; błąd

    decltype((a->x)) x36 = f2; // const double&
    //x36 = f1; błąd

    // decltype może nam pomóc w definiowaniu skomplikowanych 
    // wskaźników na funkcję.
    decltype(A::sx) x51; // double
    decltype(&A::x) x52 = &A::x; // double A::*
    x51 = 5;
    double x53 = a->*x52;

    // decltype można wykorzystać do definiowania typów.
    typedef decltype(A::sx) XX;
    XX aa = 5;

    decltype(&A::sxx) x61 = &A::sxx; // double (A::*)()
    decltype(&A::xx) x62 = &A::xx; // double A::*)()
    x61();
    (a->*x62)();
}

// Robimy dziwne rzeczy i oczekujemy normalnych od kompilatora.
void test_stupid()
{
    decltype(1/0) x71; // int, wyrażenie nie jest wyliczane. 
                       // Typ jest ustalany podczas kompilacji.
    decltype(exit(2)) x76(); // funkcja nie zostanie wywołana.
    decltype(1.0/0.0) x72; // double
    decltype((float)1.0/0.0) x73; 
    decltype(1.0f/0) x74; // float
    //decltype(float) x75; // błąd, musi być wyrażenie

    // jakkolwiek auto wydaje się bardziej naturalne do 
    // deklarowania zmiennych, decltype pozwala na 
    // stworzenie zmiennej bez jej inicjalizacji.
    decltype(5) x81; // x81 jest typu int
    x81 = 5;
    //auto x82; błąd, musi zostać zainicjalizowane
}

template <class R, class X, class Y> 
R add_old(X x, Y y)
{
    return x + y;
}

void test_template_old()
{
    auto x1 = add_old<int>(5, 5); // int
    auto x2 = add_old<double>(5, 5.0); // double
    auto x3 = add_old<double>(5.0, 5); // double
    auto x4 = add_old<double>(5.0, 5.0); // double

    std::string str1 = "1";
    std::string str2 = "1";
    auto str3 = add_old<std::string>(str1, str2);
}

// Tutaj ujawnia się prawdziwa zaleta decltype, 
// zwracany typ zależy od wyniku operacji, 
// pośrednio od typu parametrów szablonu.
template <class X, class Y> 
auto add_new(X x, Y y) -> decltype(x+y)
{
    return x + y;
}

void test_template_new()
{
    auto x1 = add_new(5, 5); // int
    auto x2 = add_new(5, 5.0); // double
    auto x3 = add_new(5.0, 5); // double
    auto x4 = add_new(5.0, 5.0); // double

    std::string str1 = "1";
    std::string str2 = "1";
    auto str3 = add_new(str1, str2);
}

int main(int argc, char* argv[])
{
    test1();
    test_template_old();
    test_template_new();
    test_stupid();
    return 0;
}

W funkcji test1 widzimy pierwszy rodzaj zastosowania. Zamiast definiować typ możemy zrzucić na kompilator by sam go określił na podstawie wyrażenia. Tak zdefiniowany za pomocą decltype typ możemy wykorzystać do określenia typu zmiennej albo do stworzenia nowego typu (linia 78). Oczywiście do deklarowania typu zmiennej lepiej nadaje się auto. Znowu auto nie wykorzystamy do tworzenia nowego typu. W przeciwieństwie do decltype słowo kluczowe auto wymaga od nas zainicjalizowania zmiennej aby zgadnąć typ (linia 103). Słowo decltype może być szczególnie ważne w przypadku korzystania ze skompilowanej struktury szablonów albo z wyrażeń lambda, gdzie ręczne podanie typu może być nie lada wyzwaniem.

W linii 96 widzimy, że delctype nie zadziała dla typu. Jest to błąd kompilacji.

W linii 64 widzimy zastosowanie nawiasów które pełnią tutaj rolę specyficznego operatora 'referencja do'. Ponieważ obiekt jest zadeklarowany jako const referencja jest też stała.

Jeśli wyrażeniem jest wywołanie funkcji to typem jest wynik funkcji (linia 49). Jeśli podamy adres funkcji typem będzie wskaźnik na funkcję.

Drugi ważny rodzaj zastosowania widzimy w test_template_new. Dla porównania mamy jakby to wyglądało po staremu test_template_old. Tutaj słowo kluczowe decltype służy do określania typu rezultatu funkcji na podstawie wyrażenia zawierającego typy z szablonu. Porównując obie wersje wydaję się, że po co tyle zachodu - o jeden dodatkowy argument do podania. Tak naprawdę siła decltype ujawnia się w skomplikowanym systemie klas szablonowych, kiedy odgadnięcie typu musi być zastosowane gdzieś głęboko. Wtedy nawet możemy nie rozumieć sensu tego dodatkowego parametru podawanego w starej wersji. W klasach szablonowych słowo kluczowe decltype pozwala nam zredukować ilość klas szablonowych, a także sprawić, że już napisane klasy będą mogły łatwiej współpracować z naszymi typami. Rozpatrzmy np. zestaw klas które służą nam do implementowania wyrażeń arytmetycznych. Jedna klasa Add jest w stanie dodawać dowolne typy, jeśli tylko mają one zaimplementowany operator +.

Podsumowując:
  • łatwiejsze tworzenie nowych typów
  • łatwiejsze deklarowanie zmiennych (z auto jeszcze łatwiejsze)
  • uproszczone korzystania z i tworzenie szablonów