Przejdź do głównej zawartości

C++11 Iterowanie


Zasadniczo iterowanie po elementach tablicy/vectora/listy itp. nie jest niczym nowym, interesującym ani pasjonującym, ot szara codzienność. Przyjrzyjmy się zatem jak robimy to najczęściej:

class Image
{
public:
  Image();
  void rotate(float angle);
  void display();
  void* serialize();
};

class ShowImage
{
public:
  void operator()(Image image)
  {
    image.display();
  }
};

//...

std::vector<Image> imageCollection(10);

for(int i=0; i<imageCollection.size(); ++i)
{
  imageCollection[i].display();
}

for( std::vector<Image>::iterator 
                        currentImage = imageCollection.begin();
                        currentImage < imageCollection.end();
                        ++currentImage )
{
  currentImage->display();
}

std::for_each(imageCollection.begin(), imageCollection.end(),
              ShowImage());

Dla naszych potrzeb stworzyliśmy specjalną klasę Image (jej implementacja oraz funkcjonalność nie jest istotna dla naszych potrzeb). Tworzymy 10 elementowy wektor obiektów klasy Image, a następnie iterujemy po nich w 3 najpopularniejsze sposoby. Traktując wektor jak zwykłą tablicę, wykorzystując iteratory, oraz za pomocą algorytmu for_each i funktora. Na chwile obecną jedną rzeczą na którą warto zwrócić uwagę to sposób inkrementacji indeksów(++i jest szybsze niż i++ gdyż nie wymaga kopiowania).
Powyższy kod nie jest niczym ciekawym, na szczęście z pomocą przychodzi nam nowy standard języka C++ czyli wyrażenia lambda oraz specjalna składnia dla pętli for. Bez zbędnych opóźnień przyjrzyjmy się implementacji.

for(auto it:imageCollection)
{
  it.display();
}

std::for_each(imageCollection.begin(), 
              imageCollection.end(),
              [](Image image){image.display();});


Zastanówmy się jak dział nowa wersja pętli for. Niepostrzeżenie wprowadziłem również nowe słówko kluczowe auto pozwala ono na automatyczną dedukcję typów (w tym przypadku typem wydedukowanym będzie Image). Znacznie bardziej interesujące jest to co znajduje się po dwukropku, jest to nasz wektor obiektów Image. Jak to działa? Otóż pętla for w tej wersji pobiera ze wskazanego kontenera iteratory begin i end a następnie dla wszystkich elementów pomiędzy nimi wykonuje kod ujęty w klamrach. Tak naprawdę jako kontenera możemy użyć czegokolwiek co dostarcza metody begin, end i operatora mniejszości dla iteratorów zwracanych przez begin i end. Całkiem proste i przyjemne.
A co z drugim przykładem? Mamy tu dobrze znany algorytm for_each przyjmujący jak zwykle iterator do elementu pierwszego i adres obszaru pamięci za ostatnim elementem w kontenerze. W miejscu funktora/wskaźnika do funkcji znajduje się wyrażenie lambda. Lambda jest specjalnym przykładem funkcji implementowanej w miejscu (ciekawostka: w objective-c mamy do czynienia z blokami). Składnie lambdy w C++11 można pokrótce przedstawić następująco [](){}. W nawiasach kwadratowych [] podajemy sposób dostępu do danych z zakresu w jakim znajduje się nasze wyrażenie ([&] - oznacza dostęp do wszystkich zmiennych przez referencję, [=] oznacza dostęp do wszystkich zmiennych przez wartość, kwantyfikatory dostępu można składać i łączyć np. [&x, =] ). Nawias okrągły () zawiera parametry naszej metody. W nawiasach klamrowych {} znajduje się ciało funkcji.
Kiedy używać danej metody? Na to pytanie nigdy nie ma prostej odpowiedzi. Z ust wielu kolegów programujących w C++ często słyszałem że traktowanie wektora jak tablice wygląda nieprofesjonalnie.

PS
Zachęcam do wykonania testów wydajnościowych dla podanych metod i podzielenia się wynikami w komentarzach

Komentarze

Popularne posty z tego bloga

Makra i preprocesor

Jako programista klasycznego C przyszło mi wielokrotnie ścierać się z makrami. Makro to zestaw instrukcji umieszczanych w kodzie są jednak interpretowane nie przez kompilator ale przez preprocesor. Preprocesor jest „pomocnikiem” kompilatora, zajmuje się on np. wstawianiem treści plików nagłówkowych do plików z kodem za pomocą instrukcji #include. Preprocesor należy rozumieć jako prymitywny edytor tekstu dokonujący „korekcji” plików z kodem źródłowym przed rozpoczęciem ich przetwarzania przez kompilator. Jakie są zalety wykorzystywania tych rozwiązań w kodzie? Tak naprawdę w języku C w czasach przed wprowadzeniem słowa kluczowego inline umożliwiały wstawianie kodu we wskazane miejsce. Należy bowiem pamiętać że każdorazowe wstawienie makroinstrukcji powoduje ingerencję w kod źródłowy (innymi słowy we wskazanym miejscu zostanie wstawiony stosowny fragment kodu). Łatwo obserwowalnym efektem częstego wykorzystywania makr w plikach z kodem jest rozrost pliku binarnego oraz jego szybsze d

C++11 Variadic templates – szablony ze zmienną liczba parametrów

Witam w nowym roku. Dziś będzie o nowym elemencie szablonów czyli o szablonach ze zmienną liczbą parametrów. Ich implementacja i zachowanie różni się nieco od klasycznych szablonów. Nowa funkcjonalność pozwala na tworzenie bezpiecznych list typów. template<typename ...Ts> void variadic_template(){} Pierwszą nowością jest zastosowanie ...(wielokropka) przy określaniu typów szablonu. W ten sposób sygnalizujemy mnogość typów. Niestety nie mamy możliwości iterowania po kolejnych typach wewnątrz naszego szablonu. Ponadto taka definicja pozwala na stworzenie/wywołanie naszej szablonowej funkcji bez typów. variadic_template<>(); Aby tego uniknąć można uciec się do następującej sztuczki: template<typename T1, typename ...Ts> void variadic_template(T1 arg, Ts... args) W powyższym przykładzie jawnie wymuszamy podanie przynajmniej jednego typ dla naszej funkcji szablonowej. Jak już wspomniałem nie możemy jawnie iterować po typach przekazanych do