C++0x część 1

C++0x to nowy standard języka C++, będący obecnie w ostatniej fazie zatwierdzania. Ponieważ zawiera on dużo zmian i nowości, zarówno po stronie samego języka jak i biblioteki standardowej, warto się z nimi zapoznać już teraz.


Stopień trudności: 2


Dowiesz się:

  • * O podstawowych, nowych elementach języka;
  • * Co nowego niesie ze sobą C++0x;
  • * Jak wygląda wsparcie C++0x przez kompilatory.


    Powinieneś wiedzieć:

  • * Jak tworzyć programy w C++;
  • * Co to są szablony;
  • * Co to są wskaźniki inteligentne;
  • * Jak używać kompilatora GCC.


    Kapitanie! C++0x na horyzoncie!

    Obecnie mówiąc "C++" większość osób ma na myśli standard C++03, zatwierdzony w 2003 roku. Była to modyfikacja standardu C++98 (z 1998 roku), zawierająca poprawki poprzedniego wydania (w tym ulepszenie wskaźnika inteligentnego std::auto_ptr). Znacznie większe zmiany miała przynieść kolejna odsłona C++, mająca się ukazać w okolicach 2008 roku. Pierwsza publikacja proponowanych zmian ukazała się w 2005 roku jako "Technical Report 1" (TR1).

    Nowy standard roboczo nazwano C++0x, gdzie docelowo "x" miał być zastąpiony ostatnią cyfrą roku wydania; jak początkowo uważano - "8". Czas jednak płynął a modyfikacji języka miało być coraz więcej. W 2010 roku komitet postanowił usunąć część "problematycznych" części specyfikacji, by wydać specyfikację możliwie szybko. Choć w C++0x nie zobaczymy m.i. konceptów (pomysłowego rozwinięcia szablonów), finalna wersja dokumentu trafiła już do zatwierdzenia. Oficjalne wydanie/zatwierdzenie standardu powinno nastąpić jeszcze tego lata. W ten sposób narodzi się C++11... a może C++0B? :-)

    C++0x z lotu ptaka

    Kolejna odsłona języka przynosi liczne modyfikacje, czasami wydające się prozaiczne, jednak niosące za sobą znaczące zmiany w myśleniu o wytwarzaniu oprogramowania w C++. Chyba jedną z najbardziej oczekiwanych zmian jest dodanie modelu pamięci oraz wsparcia dla wątków. Od wielu lat (względnie) proste aplikacje sekwencyjne stają się wspomnieniem z przeszłości. Do tej pory korzystanie z mechanizmu wątków wymagało używania nieprzenośnych rozwiązań takich jak pthreads (często nie pisanych obiektowo), lub zewnętrznych bibliotek, takich jak boost::thread, obudowujących mechanizmy systemowe. Obecnie wątki są częścią języka, biblioteka standardowa zaś dostarcza mechanizmów do sterowania przepływem danych, zgodnych z najnowszymi trendami. Dodano też możliwości nieosiągalne w C++03, takie jak przerzucanie wyjątków pomiędzy wątkami.

    Kolejną dużą zmianą są wyrażenia lambda. Umożliwiają one pisanie prostych funkcji bezpośrednio w miejscu ich użycia, co w sposób znaczący poprawia czytelność kodu podczas używania funktorów w algorytmach biblioteki standardowej. Stosowanie lambd ułatwia także tworzenie wątków pomocniczych do wykonywania dodatkowych operacji - teraz utworzenie i uruchomienie wątku wykonującego jakąś funkcję to tylko 1 linijka!

    Nieodzownym elementem stosowania wyjątków (zarówno świadomego jak i nieświadomego) jest zapewnianie poprawnego zwalniania zasobów takich jak pamięć, gniazda, połączenia z bazami danych, etc... W C++03 można to dość łatwo osiągnąć stosując RAII (Resource Allocation Is Initialization) w połączeniu ze wskaźnikami inteligentnymi. C++03 definiował jednak wyłącznie jeden inteligentny wskaźnik - std::auto_ptr - co w praktyce było niewystarczające w większości przypadków. Ponownie na ratunek przychodził zestaw bibliotek Boost - konkretnie biblioteka Smart Pointer - a w niej: boost::shared_ptr, boost::scoped_ptr, etc... Teraz mechanizmy te są częścią biblioteki standardowej.

    Ogromnym krokiem w myśleniu o programach pisanych w C++0x jest pojawienie się semantyki przenoszenia, podobnej do kopiowania, ale "niszczącej" oryginał. Jest to ogromna optymalizacja ułatwiająca pisanie prostego ale wydajnego kodu. Od tej pory funkcja tworząca std::vector dowolnych elementów i zwracająca go przez wartość nie będzie powodowała żadnych strat wydajności (brak konieczności tworzenia kopii, kiedy oryginał ma być zaraz zniszczony)! Umożliwi także wstawianie wskaźników inteligentnych, nie zliczających referencji, do standardowych kontenerów.

    Oprócz "grubych" zmian, pojawił się także bardzo pokaźny zestaw przydatnych "drobiazgów". Programiści C++0x będą mogli inicjalizować kontenery biblioteki standardowej tak, jakby były to zwykłe tablice. Pojawiły się generatory liczb pseudolosowych z prawdziwego zdarzenia. Dostaliśmy do dyspozycji pełen zestaw funkcji do zarządzania czasem oraz interwałami. Przekazywanie parametrów między konstruktorami/funkcjami (ang.: forwarding) może być wykonane z zerowym narzutem czasu wykonania. Szablony mogą mieć nieokreśloną z góry liczbę parametrów (podobnie jak funkcje z parametrem "...", lecz wyznaczane w czasie kompilacji). Elementy składowe typów wyliczeniowych przestały "wyciekać" do zakresu ich deklaracji - MyEnumType::SOME_VALUE jest teraz poprawnym (i preferowanym) zapisem. Programista dostanie do ręki dodatkowe słowa kluczowe do wymuszania i sterowania (świadomym) używaniem takich operacji jak konwersji, przeciążania metod wirtualnych czy tworzenia/blokowania domyślnych operacji i konstruktorów. Pojawiło się słowo kluczowe dla funkcji i metod nie zgłaszających wyjątków. W odpowiedzi na powszechność użycia pojawiły się także napisy kodowane w UTF-8, UTF-16 oraz UTF-32. Powstał mechanizm zapewniający dokładnie jedno wywołanie danego fragmentu kodu (inicjalizację), działającego również w przypadku użycia z wielu wątków. Zdefiniowano pętlę for() automatycznie iterującą po zakresach i tablicach w stylu C. No i oczywiście rozwiązano problem operatora >> w deklaracjach zagnieżdżonych szablonów (np: Szablon1<Szablon2<int>>). ;-)

    Kompilatory i C++0x

    Teoria teorią, a życie życiem. Pierwszym pytaniem, jakie się nasuwa jest wsparcie dla C++0x po stronie kompilatorów. Tu sprawa wygląda bardzo różnie. Przykładowe fragmenty kodu, pisane na potrzeby wewnętrznego szkolenia, kompilują się lub nie pod różnymi kompilatorami. Łącznie powstało 91 przykładowych mini-aplikacji, kompilowalnych za pomocą GCC 4.6. ICC 12.0.4 (najnowszy kompilator Intela) poradził sobie z 21 z nich. GCC 4.4 podobnie jak 4.5 skompilowały zaledwie 8. Clang++ 1.1 poradził sobie z 9, wersja 2.9 (najnowsza) zaś z 8 (tak - mniej). Visual Studio 2010 skompilował 34.

    Niniejszy cykl artykułów z założenia ma być praktycznym spotkaniem z C++0x, nie zaś suchym wykładem ze specyfikacji. Aby ten cel osiągnąć, zdecydowano się na prezentację najważniejszych (zdaniem autorów) elementów języka (i biblioteki standardowej), jednocześnie wspieranych przez wybrany kompilator.

    Ponieważ GCC-4.6 wydaje się mieć obecnie najlepsze wsparcie dla C++0x oraz jest darmowy, na niego właśnie padł wybór. Wszystkie prezentowane przykłady będą więc kompilowane przy jego pomocy. Niestety oznacza to również, że część zagadnień zostanie przemilczana, lub jedynie wspomniana, ponieważ nie jest obecnie implementowana przez kompilator.

    Kompilowanie przykładów
    Do kompilowania przykładów należy użyć kompilatora GCC w wersji 4.6, ustawiając standard języka na C++0x:
    g++-4.6 -Wall -std=c++0x src.cpp
    Większość prezentowanych fragmentów kodu NIE będzie działać na starszych wersjach, które miały minimalne lub żadne wsparcie dla C++0x. W przypadku nowszych wersji GCC niektóre przykłady mogą wymagać drobnych modyfikacji. Pamiętajmy, że C++0x nie jest jeszcze zatwierdzonym standardem a i jego fragmenty mogą ulec zmianie. Oprócz tego GCC nie jest jeszcze w pełni zgodny z najnowszą propozycją standardu języka.

    Należy jednak pamiętać, iż wciąż jest bardzo wiele cech C++0x, które nie są wspierane przez większość popularnych kompilatorów. Będąc uczciwym - większość powszechnie stosowanych kompilatorów implementuje jedynie wycinek języka. Sam GCC-4.6 obecnie zupełnie nie wspiera takich elementów jak: aliasy do szablonów, dziedziczenie konstruktorów, wyrażenie regularne (choć podstawowy kod się już kompiluje, nadal nie działa) oraz posiada jedynie częściowe wsparcie m.i. dla: wątków, napisów kodowanych w UTF-16 i UTF-32, konwersji napisów, etc...

    Różne oblicza pustki

    Nim przejdziemy do C++0x, zastanówmy się chwilę nad listingiem 1a.

    void f(int) { cout<<"1"; } // wersja 1
    void f(void*) { cout<<"2"; } // wersja 2
    // ...
    f(NULL);
    Listing 1a: Która wersja zostanie wykonana?

    Która z przeciążonych funkcji zostanie wykonana? Osoby przyzwyczajone do języka C odpowiedzą zapewne "2". W C makro NULL było przeważnie implementowane jako (void*)0. Większość programistów C++ odpowie "1", ponieważ NULL jest tu przeważnie definiowany jako 0. Prawidłowa odpowiedź brzmi "to zależy". Standard C++ stawia pewne wymagania przed wartością stałej NULL, ale nie definiuje jej postaci jawnie. W praktyce kompilatory mogą ją różnie implementować. Jako ciekawy przykład można tu przytoczyć domyślne zachowanie kompilatora GCC, który... w ogóle nie kompiluje takiego kodu! Zwraca on ostrzeżenie o jego niejednoznaczności. Wywołania f(0) oraz f((void*)0) za to się kompilują bez problemu i robią to, czego programista się spodziewa. W praktyce zaistniała sytuacja powoduje też inne, "ciekawe" efekty uboczne, jak na przykład konwersję NULL do typu całkowitoliczbowego (int i=NULL;), jeśli NULL jest zdefiniowany jako 0.

    Jak nie trudno odgadnąć takie zachowanie powoduje masę problemów praktycznych, szczególnie gdy pisany kod ma być przenośny. C++0x w końcu porządkuje zaistniałą sytuację poprzez jasne zdefiniowanie wskaźnika nullptr, będącego typu std::nullptr_t. Zastosowanie nullptr przedstawiono na listingu 1b.

    void f(int) { cout<<"1"; } // wersja 1
    void f(void*) { cout<<"2"; } // wersja 2
    struct AnyClass;
    // ...
    f(nullptr); // ok (wersja 2)
    int *v1=nullptr; // ok
    AnyClass *v2=nullptr; // ok
    bool v3=nullptr; // ok (v3==false)
    //int v4=nullptr; // "error: cannot convert to numeric type"
    std::nullptr_t ptr=nullptr; // ok
    Listing 1b: nullptr w akcji.

    Listing 1b pokazuje, iż nowa stała nullptr zachowuje się zgodnie z intuicją, bez kruczków i rozbieżności charakterystycznych dla stałej NULL. Kod stał się jednocześnie prosty i przenośny. Od momentu wejścia C++0x wyłącznie stała nullptr powinna być używana. W kolejnych przykładach będziemy stosować jedynie nullptr.

    Typ 'auto'

    Standard C++0x przynosi ze sobą nowe znaczenie słowa kluczowego auto. Słowo to istniało już wcześniej w języku, jednak nie było specjalnie przydatne i przez to praktycznie nieużywane. Obecnie auto może być wykorzystane również jako deklaracja typu zmiennej co może trochę dziwić bo co miałby znaczyć zapis taki jak w listingu 2a.

    auto i; // C++0x
    Listing 2a: co oznaczałby taki zapis?

    Czy i jest tutaj zmienną typu int, float czy może char? Tego oczywiście nie wiadomo i taki kod nie skompiluje się. Bardzo często jednak mamy deklaracje z inicjalizacją (listing 2b).

    int i=5;
    int j=rand();
    Listing 2b: przykładowe deklaracje zmiennych w C++03.

    Gdzie użycie int wydaje się nadmiarowe bo zarówno liczba 5 jak i wartość zwracana przez funkcję rand() jest typu int. To jest właśnie to miejsce, gdzie auto przychodzi z pomocą. Być może pisanie auto w miejscu int nie wydaje się wielkim uproszczeniem, ale już przykładowa pętla z wykorzystaniem iteratora, owszem - listing 2c.

    for (std::vector<std::string>::iterator it=v.begin(); it!=v.end(); ++it); // C++03
    for (auto it=v.begin(); it!=v.end(); ++it); // C++0x
    Listing 2c: użycie iteratorów w C++03 i C++0x.

    Zapis z listingu 2c nie pozwala niestety wybrać pomiędzy iterator i const_iterator. Założenie jest takie, że jeśli kontener jest const brany jest const_iterator, a w przeciwnym wypadku iterator. Słowo kluczowe auto bedzie pojawiać się w dalszych częściach tego cyklu upraszczając nieraz bardzo skomplikowane wyrażenia.

    Uogólnione wyrażenia stałe

    Programiści C++ znają i używają kwalifikatora const od zawsze. Zapewnia on lepszą kontrolę na tym, co się faktycznie dzieje w programie, kto może modyfikować dany obszar pamięci, etc... const pilnuje programisty przed pewnymi błędami w czasie kodowania, które można wykryć w trakcie kompilacji.

    Czasem potrzebujemy jednak pewnego, specyficznego rodzaju stałej - stałej czasu kompilacji, której moglibyśmy używać potem jak wartości natychmiastowej. Przykładem jest zastosowanie stałej do definiowania rozmiaru tablicy, jak pokazano na listingu 3a, lub przekazywanie jako parametr do szablonu.

    double tab1[42]; // ok
    const int n=42;
    double tab2[n]; // niepoprawny kod w C++ (na niektórych kompilatorach działa)
    Listing 3a: tworzenie tablic o tym samym rozmiarze.

    "Zmienna" pomocnicza (a właściwie stała czasu kompilacji) przydaje się np. gdy mamy kilka tablic, tego samego rozmiaru, do utworzenia. Tu z pomocą przychodzi nam C++0x i uogólnione wyrażenia stałe. Wyrażenie stałe deklaruje słowo kluczowe constexpr, jak pokazano na listingu 3b.

    constexpr int n=42;
    double tab1[n]; // ok w C++0x
    double tab2[n]; // ok w C++0x
    Listing 3b: stała definiująca rozmiar tablicy.

    Ale to nie koniec! W końcu wyrażenia stałe, to nie tylko stałe wartości podane wprost. C++0x umożliwia zdefiniowanie funkcji, będącej wyrażeniem stałym. Jako przykład stwórzmy funkcję zwracającą daną liczbę, podniesioną do podanej potęgi i jej wyniku użyjmy do zdefiniowania rozmiaru 2 tablic. Przykładowy kod zaprezentowany jest na listingu 3c.

    constexpr unsigned int power(unsigned int a, unsigned int n)
    {
     return (n==0)?1:a*power(a, n-1);
    }
    double tab1[ power(3, 5) ];
    double tab2[ power(3, 6) ];
    Listing 3c: funkcja jako wyrażenie stałe.

    Choć uzyskaliśmy pożądany efekt, osoby znające metaprogramowanie za pomocą szablonów mogą w tym miejscu zaprotestować. Taki sam efekt da się przecież uzyskać za pomocą odpowiedniego metaprogramu w C++03 - po co nam C++0x do tego? Otóż pojawia się jedna zasadnicza różnica - metaprogram może działać wyłącznie na stałych czasu kompilacji, zaś funkcja constexpr może operować również na zmiennych czasu wykonania. Oznacza to, iż możemy w programie zapytać użytkownika o dwie liczby a oraz b i wypisać wynik na ekranie ponownie stosując tą samą power(a,b) a tego za pomocą "klasycznego" metaprogramu zrobić się już nie da. Oczywiście podanie zmiennych czasu wykonania spowoduje, iż nasz wynik będzie również zmienną czasu wykonania i nie będzie można go wykorzystać np. jako parametr szablonu.

    Gdzie więc przebiega granica pomiędzy czasem kompilacji a czasem wykonania? C++0x definiuje kilka prostych zasad, definiujących zachowanie wyrażeń constexpr. W skrócie wygląda to następująco:

  • 1. Wartości natychmiastowe są wyrażeniami stałymi.
  • 2. Wartości wyliczane jedynie na podstawie wartości stałych są wartościami stałymi.
  • 3. Aby funkcja constexpr była wartością stałą musi zwracać wartość oraz mieć ciało postaci return (wartość-stała).
  • 4. Jeśli wyrażenia nie da się rozwinąć do pewnej, zdefiniowanej przez implementację, głębokości wyrażenie nie jest stałe, a obliczenia przenoszone są do czasu wykonania.

    Choć wyrażenia stałe są intuicyjne, jest pewien subtelny problem, na jaki można natrafić podczas kodowania. Listing 3d przedstawia nieco zmodyfikowany program z listingu 3c. Choć na pierwszy rzut oka funkcje wydają się identyczne i w obu przypadkach poprawnie się kompilują i linkują, pierwszy program działa poprawnie (deklaruje 2 tablice), drugi zaś zamiast wypisać wartość na ekranie uporczywie pokazuje błąd segmentacji. Czyżbyśmy znaleźli błąd w kompilatorze?

    constexpr unsigned int power1(unsigned int a, unsigned int n)
    {
     return (n==0)?1:a*power1(a, n);
    }
    // ...
    cout<<power1(3, 5) <<endl;
    Listing 3d: nieco zmodyfikowana funkcja power z listingu 3c - co jest nie tak?

    Nie zupełnie... Na pomoc z odpowiedzią, co się właściwie stało, przychodzi nam punkt 4 reguł wyliczania wyrażeń stałych. W funkcji power1 programista przez pomyłkę zapomniał zmniejszyć wartość zmiennej n o 1, przed ponownym wywołaniem rekurencyjnym, co z kolei spowodowało, iż w czasie kompilacji przekroczyliśmy limit zagłębień, przewidziany przez nasz kompilator. Zgodnie z zasadami zaś, kompilator odłożył obliczenia wartości naszej funkcji, do czasu uruchomienia programu. Po uruchomieniu zaś funkcja wołała rekurencyjnie samą siebie aż do przepełnienia stosu i wywołania błędu ochrony pamięci. Błąd prosty, ale efekt (w pierwszej chwili) nieco zaskakujący.

    Tak więc mamy funkcje, będące wyrażeniami stałym. A co z metodami i obiektami? Nie ma problemu - dokładnie tym samym sposobem możemy zadeklarować dowolną metodę jako wyrażenie stałe i używać w czasie kompilacji. Szczególnym przypadkiem jest konstruktor, który także może być wyrażeniem stałym (pod warunkiem, że jego ciało jest puste), mimo iż nie posiada wartości zwracanej.

    Niech ktoś zatrzyma tą kompilację!

    Każdy programista C/C++ zna makrodefinicję assert. Umiejętne stosowanie jej bardzo ułatwia znajdywanie błędów podczas wykonywania programu w wersji debug, jednocześnie nie powodując narzutu w wersji release. W praktyce pojawiają się jednak często sytuacje, w których już na etapie kompilacji jesteśmy w stanie wykryć błąd, a jak wiadomo im wcześniej błąd zostanie znaleziony tym tańsze/szybsze będzie jego poprawienie.

    Weźmy dla przykładu rodzinę klas reprezentujących algebraiczne wektory o stałym rozmiarze. Zamiast tworzyć osobne klasy dla każdego z przypadków, tworzymy uniwersalny szablon, jak pokazano na listing 4a.

    template<uint8_t N>
    class AlgVec
    {
    public:
     // konstruktory, metody, etc...
    private:
     double dim_[N];
    };
    Listing 4a: szablon klasy wektora o znanym z góry rozmiarze (i.e. liczbę wymiarów).

    Podobnie jak parametry czasu wykonania, parametry szablonu wymagają sprawdzenia. Kwestię podania ujemnego rozmiaru tablicy łatwo można rozwiązać wymuszając podanie typu bez znaku (tu: uint8_t). Problemem pozostaje jednak 0, które choć nie spowoduje błędu kompilacji, nie ma specjalnie sensu w kontekście algebraicznego wektora.

    Właśnie w tym miejscu z pomocą przychodzi nam nowość, oferowana przez C++0x - static_assert, czyli asercja czasu kompilacji. Na listingu 4b pokazano prostą modyfikację programu z listingu 4a, sprawdzającą czy podany rozmiar wektora jest poprawny (i.e. dodatni).

    template<uint8_t N>
    class AlgVec
    {
    public:
     // konstrutory, metody, etc...
    private:
     static_assert(N>0, "N must be positive value");
     double dim_[N];
    };
    Listing 4b: wykorzystanie asercji statycznej do sprawdzenia parametru szablonu.

    Przydatnym udogodnieniem używania static_assert jest możliwość wyświetlenia dowolnego komunikatu tekstowego, kiedy asercja się nie powiedzie. W przypadku klasy AlgVec otrzymany komunikat może wyglądać jak ten na listingu 4c.

    test.cpp: In instantiation of ‘AlgVec<0u>’:
    test.cpp:22:13: instantiated from here
    test.cpp:11:3: error: static assertion failed: "N must be a positive value"
    Listing 4c: niepowiedzenie się asercji czasu kompilacji - przykładowy wynik.

    Osoby znające biblioteki Boost zapewne znają także makrodefinicję BOOST_STATIC_ASSERT, zatrzymującą kompilację, gdy podany warunek nie zostanie spełniony. Niestety makro to nie pokazuje komunikatu użytkownika, lecz znacznie mniej czytelny błąd kompilacji (np.: o nieznanym rozmiarze struktury, której nazwa sugeruje, że chodzi właśnie o asercję czasu kompilacji). Mechanizm ten jest też niestandardowy (i.e. nie jest częścią języka).

    Szablony bez ograniczeń

    Mówiąc o asercjach czasu kompilacji, płynnie przechodzimy do kolejnego zagadnienia ściśle związanego z metaprogramowaniem - szablonów o dowolnej liczby parametrów.

    Pisząc metaprogramy często napotykamy potrzebę stworzenia kolekcji, krotki lub innego "kontenera", z którymi chcemy coś zrobić. Niestety szablony C++03 mogą mieć tylko z góry określoną liczbę parametrów, które programista musi jawnie zdefiniować na etapie tworzenia kodu. Aby ułatwić to zadanie dodatkowe biblioteki dostarczają pewnych "hacków językowych", mających załatać ową dziurę. W ten sposób powstała między innymi klasa boost::tuple czy biblioteka MPL z rodziny Boost, z klasami takimi jak boost::mpl::vector, czy boost::mpl::list.

    Mechanizmy te jednak nie zawsze były wygodne w użyciu. Przede wszystkim jednak miały pewne problematyczne ograniczenia, jak na przykład maksymalny rozmiar wektora MPL. Używanie ich niejednokrotnie także wymagało stosowania rozwlekłego zapisu.

    Wspomniane problemy przechodzą do historii wraz z pojawieniem się szablonów zmiennej długości (ang. variable templates) w C++0x.

    Zmienną liczbę parametrów w szablonie deklaruje się, podobnie jak dla funkcji w C, za pomocą operatora .... Przykładowa deklaracja szablonu, który wypisuje na ekranie liczbę swoich parametrów przedstawiona została na listingu 5a.

    template<typename ...Args>
    void printCount(Args... args)
    {
     cout<<sizeof...(args)<<endl;
    }
    // ...,
    printCount(); // 0
    printCount(42); // 1
    printCount(42, "hello"); // 2
    Listing 5a: wyświetlania liczbę parametrów szablonu.

    Listę parametrów możemy także "rozpakować", tak jakby kolejne element były podane jawnie i oddzielone od siebie przecinkiem. Przykładową funkcję "rozpakowującą" 2 parametry pokazano na listingu 5b.

    template<typename T, typename U>
    void print2(const T &t, const U &u)
    {
     cout<<"["<<t<<"; "<<u<<"]"<<endl;
    }

    template<typename ...Args>
    void printSome(Args... args)
    {
     print2(args...);
    }
    // ...
    printSome("abc", 3.14);
    Listing 5b: "rozpakowywanie" parametrów szablonu.

    Jak widać na listingach 5a i 5b operator ... może mieć dwa, różne znaczenia. Jeśli operator występuje po lewej stronie wyrażenia, oznacza on deklarację parametrów (typu). Operator ... występujący po prawej stronie oznacza zaś "rozpakowanie" typu do pojedynczych elementów składowych.

    Program z listingu 5b jest mało elastyczny. Choć pozornie możemy podać dowolną liczbę parametrów do naszego szablonu printSome, podanie innej liczby parametrów niż 2 spowoduje błąd kompilacji (print2 istnieje tylko w wersji 2-parametrowej). Aby obejść ten problem, przetwarzanie parametrów jest przeważnie realizowane jako przechodzenie po liście jednokierunkowej, podobnie jak ma to miejsce w językach funkcyjnych: mając "głowę" (pierwszy element) oraz "ogon" (pozostała część listy). Zauważmy, iż gdyby nieco zmienić funkcję print2, aby brała 1 parametr szablonowy oraz listę (która w szczególności może być pusta) i wywoływała samą siebie rekurencyjnie, aż do wyczerpania parametrów, można by wypisać dowolną liczbę parametrów! Aby obsłużyć także przypadek braku parametrów, dodajmy jeszcze funkcję o tej samej nazwie, ale nie przyjmującą żadnych parametrów. Wywołanie to przerwie rekurencyjne przetwarzanie parametrów. Gotowy kod pokazano na listingu 5c.

    void printList(void)
    {
     cout<<"]"<<endl;
    }

    template<typename Head, typename ...Tail>
    void printList(Head h, Tail... t)
    {
     cout<<h;
     if( sizeof...(t)>0 )
      cout<<"; ";
     printList(t...);
    }

    template<typename ...Args>
    void printSome(Args... args)
    {
     cout<<"[";
     printList(args...);
    }
    // ...
    printSome("abc", 3.14, 2.72);
    Listing 5c: przetwarzanie listy parametrów jako głowy i ogona.

    Należy w tym miejscu również zaznaczyć, iż mówiąc o rekurencji, mówimy tu o "wywołaniach" w czasie kompilacji. Funkcja nie zostanie faktycznie zawołana rekurencyjnie w trakcie wykonania. Kompilator prawdopodobnie zamieni ów kod na zwykłą pętlę, napisany kod zaś będzie jedynie reprezentacją logiki przetwarzania.

    Kod z listingu 5c jest już tylko o krok od stworzenia idealnego odpowiednika funkcji printf, języka C, opartego na szablonach. Zamieniając operator ... funkcji C na szablon zmiennej długości otrzymujemy pełne bezpieczeństwo typów, jednocześnie zachowując znaną i elastyczną składnię. Nieco uproszczony przykład takiego kodu został przedstawiony na listingu 5d.

    void typeSafePrintf(const char *format)
    {
     if(format==nullptr)
      throw std::runtime_error("NULL format");

     for(const char *it=format; *it!=0; ++it)
     {
      if(*it=='%' && *(it+1)=='%')
       throw std::runtime_error("no more parameters to the format");
      else
       cout<<*it;
     }
    }

    template<typename Head, typename ...Args>
    void typeSafePrintf(const char *format, Head h, Args... args)
    {
     if(format==nullptr)
      throw std::runtime_error("NULL format");

     for(const char *it=format; *it!=0; ++it)
     {
      if(*it=='%' && *(it+1)=='%')
      {
       cout<<h;
       typeSafePrintf(it+2, args...);
       return;
      }
      cout<<*it;
     }
     throw std::runtime_error("too many paramters to the given format");
    }
    // ...
    typeSafePrintf("%% says: %%\n", "Deep Thought", 42);
    Listing 5d: (uproszczona) implementacja funkcji printf, bezpieczna ze względu na typy.

    To jednak nie koniec możliwości tego mechanizmu. Okazuje się on również niezbędny przy przekazywaniu parametrów oraz tworzeniu generycznych klas opakowujących (ang. wrapper). Zastosowania te zostaną szerzej omówione w kolejnych odcinkach niniejszej serii.

    Na koniec drobna uwaga natury stylistycznej. Wszystkie powyższe przykłady zostały przygotowane, aby możliwie jasno przedstawić główną ideę jednocześnie nie zaciemniając składni. Pisząc faktyczny kod należy pamiętać o dodaniu kwalifikatora const do parametrów oraz gdzie to tylko możliwe, zastąpić kopiowania przekazywaniem przez referencje. Takie podejście znacznie poprawi jakość kodu oraz uczyni interface samo opisującym się.

    Krotki

    W matematyce i informatyce krotka (ang. tuple) stanowi uporządkowany ciąg wartości. W teorii zbiorów (uporządkowana) n-krotka jest sekwencją n elementów, gdzie n jest liczbą dodatnią. W kontekście języków programowania krotka to struktura danych służąca do przechowywania stałych wartości o różnych typach. Struktury te stanowią część wielu języków programowania m. in. Python, Ruby, C++0x. Przykład utworzenia krotki w C++0x został przedstawiony na listingu 6a.

    std::tuple<int, float, std::string> t(2, 3.14, "Hello");
    Listing 6a: przykład utworzenia krotki w C++0x.

    Istnieje także możliwość utworzenia krotki bez definiowania jej zawartości, ale tylko pod warunkiem, że jej elementy posiadają zdefiniowane domyślne konstruktory. Przykład utworzenia krotki z wartościami domyślnymi został przedstawiony na listingu 6b.

    std::tuple<int, float, std::string> t; // domyślna inicjalizacja z (0, 0.0 string())
    Listing 6b: utworzenie krotki z wartościami domyślnymi.

    W powyższych przykładach typy elementów przechowywanych w krotce zostały zdefiniowane w sposób jawny. W C++0x krotkę można utworzyć bez jawnego podania typów przechowywanych elementów. Zostaną one "wydedukowane" przez kompilator. Listing 6c przedstawia przykład niejawnej inicjalizacji typów w krotce.

    auto t std::make_tuple(2, 3.14); // t będzie typem std::tuple<int, double>
    Listing 6c: niejawna inicjalizacja typów w krotce.

    Dostęp do elementów krotki można uzyskać poprzez metodę get(). Krotkę t z listingu 6c można "rozpakować" do pojedynczych zmiennych w sposób pokazany na listingu 6d.

    int i = std::get<0>(t);
    double d = std::get<1>(t);
    std::string s = std::get<2>(t);
    Listing 6d: dostęp do elementów krotki - odczyt.

    Do "rozpakowywania" elementów krotki bardzo przydatna może się okazać metoda tie(). Metoda ta jako argumenty przyjmuje zmienne do których mają zostać przypisane kolejne wartości elementów przechowywanych w krotce. Dodatkowo można pominąć przypisanie wybranych elementów poprzez podanie jako argumentu "std::ignore". Użycie metody tie() zostało przedstawione na listingu 6e.

    int i;
    std::string s;
    tie(i, std::ignore, s) = std::make_tuple(2, 3.14, "Hello");
    // i == 2, s == "Hello"
    Listing 6e: dostęp do elementów krotki - odczyt.

    Podobnie jeśli chodzi o zapisanie wartości do elementów krotki z listingu 6b należy się posłużyć metodą get() (listing 6f).

    std::get<0>(t) = 2;
    std::get<1>(t) = 3.14;
    std::get<2>(t) = "Hello";
    Listing 6f: dostęp do elementów krotki - zapis.

    Często używaną krotką jest krotka składająca się z dwóch elementów - czyli para. Biblioteka standardowa dostarcza wsparcia dla przechowywania pary elementów w postaci typu std::pair. Para z biblioteki standardowej może zostać użyta do inicjalizacji krotki (ale nie na odwrót) - listing 6g.

    std::pair<int, double> p(3, 2.7);
    std::tuple<int, double> t(p);
    Listing 6g: inicjalizacja std::tuple za pomocą std::pair.

    Dla krotek zdefiniowany jest operator przypisania, jeśli dwie krotki są tego samego typu oraz przechowywane elementy posiadają konstruktor kopiujący. Dodatkowo każdy typ elementu krotki po prawej stronie operatora przypisania musi być konwertowalny do typu elementu po lewej stronie operatora przypisania lub posiadać odpowiedni konstruktor. Przykład użycia operatora przypisania dla krotki został zawarty na listingu 6h.

    tuple<int , double, string > t1;
    tuple<char, short , const char *> t2('X', 2, "Hello");
    t1 = t2;
    Listing 6h: operator przypisania dla krotki.

    Dla przykładu z listingu 6h dwa pierwsze elementy krotki mogą zostać skonwertowane, a trzeci może być utworzony z const char *. Dla typu std::tuple zdefiniowane zostały także następujące operatory porównania: ==, !=, <=, <, > oraz >= .

    Krotki są wykorzystywane (pośrednio lub bezpośrednio) wszędzie tam, gdzie istnieje potrzeba przechowywania różnorodnego zbioru elementów bez definiowania do tego celu konkretnej klasy. Na przykład krotki są wykorzystywane w std::function i std::bind do przechowywania argumentów.

    Jako ciekawostkę warto przytoczyć, iż std::tuple, podobnie jak wiele rozwiązań standardowych w C++0x, posiada rodowód rodziny bibliotek Boost. Klasa ta pojawiła się tam jako logiczne rozwinięcie koncepcji std::pair.

    Podsumowanie

    Niniejszy artykuł wprowadził czytelnika w świat C++0x. Zostały ogólnie przedstawione główne zmiany zarówno samego języka jak i biblioteki standardowej. Pierwsza część objęła podstawowe elementy języka, jakie się pojawiły w nowej propozycji standardu. W dalszych częściach serii elementy te będą wykorzystywane jako części składowe kolejnych zagadnień.

    W kolejnym odcinku przedstawione zostaną rozszerzenia i uogólnienia inicjalizowania obiektów oraz kolekcji obiektów (w tym kontenerów biblioteki standardowej oraz własnych). Zostanie także staranie omówione zagadnienie wskaźników inteligentnych. Nowe cechy języka zostaną tu przedstawione w zestawieniu z typowymi błędami, popełnianymi przez programistów C++. Na koniec przedstawimy metodę optymalizacji czasu kompilacji szablonów. Jest to umiejętność szczególnie ważna podczas stosowania zagnieżdżonych szablonów oraz tworzeniu za ich pomocą metaprogramów.


    Bartosz Szurgot
    Absolwent Informatyki wydziału Informatyki i Zarządzania Politechniki Wrocławskiej. Obecnie pracuje we Wrocławskim Centrum Sieciowo-Superkomputerowym jako programista. Główne zainteresowania techniczne to: programowanie, Linux, urządzenia wbudowane oraz elektronika. W wolnym czasie tworzy oprogramowanie open-source oraz układy elektroniczne. W C++ programuje od 9 lat.
    Kontakt z autorem: bartek.szurgot@baszerr.org
    Strona domowa autora: http://www.baszerr.org

    Mariusz Uchroński
    Absolwent Elektroniki i Telekomunikacji wydziału Elektroniki Politechniki Wrocławskiej oraz pracownik Wrocławskiego Centrum Sieciowo-Superkomputerowego. Główne nurty zainteresowań technicznych to: programowanie oraz obliczenia HPC, a w szczególności programowanie GPU w CUDA i OpenCL. W C++ programuje od 5 lat.
    Kontakt z autorem: mariusz.uchronski@gmail.com

    Wojciech Waga
    Absolwent Informatyki na wydziale Matematyki i Informatyki Uniwersytetu Wrocławskiego, obecnie słuchacz studiów doktoranckich biologii molekularnej UWr oraz pracownik Wrocławskiego Centrum Sieciowo-Superkomputerowego. Programuje w C++ od 11 lat.
    Kontakt z autorem: wojciech.waga@gmail.com