MPU: begin i end

Według obietnicy w pierwszej notce odnośnie MPU chciałbym przedstawić najczęściej wykorzystywane element, które ułatwiają wykorzystywanie standardowych algorytmów operujących na zakresach dla danych przechowywanych w zwykłych tablicach.

Oczywiście żaden problem, do tej pory (pomijając wszelkie biblioteki), tablice w łatwy sposób mogły być używane jako zakresy w dowolnej funkcji algorytmu z STL-a. Przeważnie robiło się to w prosty sposób, podając wskaźnik na początkowy i ostatni + 1 element tablicy:

int tab[5] = { 1, 2, 3, 4, 5 };
 
int* tab_begin = tab;
int* tab_end = tab + sizeof(tab)/sizeof(tab[0]);
 
std::copy(tab_begin, tab_end, std::ostream_iterator<int>(std::cout, " "));

Wszystko fajnie, w oczy rzuca się tylko paskudny „trick” z sizeof do pobierania ilości elementów w tablicy, który można bardziej elegancko i cpplusowo zastąpić konstrukcja templejtową, chociażby tą z MPU:

template<typename T, size_t N>
inline size_t Count(const T (&)[N]) {
	return N;
}

O dziwo, o konstrukcji tej pisałem już dawno w notce Algorytmy STL na tablicach, gdzie również poruszyłem problem i przedstawiłem rozwiązanie jakie tutaj chce zaprezentować. Czyżby czasem tamta konstrukcja niewpadła automatycznie do worka MPU? ;)

Wspomniana wyżej notatka zawiera poniekąd cała esencję jaka miała zostać przekazana w niniejszej. Niema potrzeby powielania tekstu i kodu. Jednym z pierwszych elementów MPU był właśnie przedstawiony Counter i funkcję wyznaczające zakres z tablic.

Takowe funkcje wraz z wersjami dla tablic i zwykłych kontenerów pozwalały w prosty sposób traktować tablice jako pełnoprawny zakres, a także bezproblemowo wymieniać w implementacji użycie wektora na tablice i odwrotnie – o ile oczywiście korzystana z tych akcesoriów.

Obecnie standardowa biblioteka języka C++ zawiera takowe konstrukcje: std::begin i std::end. Implementacje zawarte w STL dokładnie odzwierciedlają przedstawione wyżej wersje (tablicowe). Wersje dla standardowych kontenerów wprawdzie także, bo odkąd w języku istnieje operator decltype oraz auto, owe implementacje wyglądają bardzo prosto – są tylko prostym proxy/przekierowaniem na wywołanie odpowiedniej funkcji kontenera:

template<typename C>
inline auto begin(C& c) -> decltype(c.begin()) {
	return c.begin();
}
 
template<typename C>
inline auto end(C& c) -> decltype(c.end()) {
	return c.end();
}

Wersje dla starego C++ (C++98) są nieco bardziej rozbudowane. W MPU zastosowano kilka rozwiązań, w zasadzie dwa – jedno proste, drugie bardziej skomplikowane.

W pierwszym uznano, że użytkownicy są mądrzy i wiedza jak używać dane im narzędzia, zatem ustalono, że będą dostępne tylko dwie implementacje – jedna dla tablic jak wyżej, druga, będąca przeciążaniem dla innych typów – w zamyśle dla kontenerów:

template<typename C>
inline typename C::iterator begin(C& c) {
	return c.begin();
}
 
template<typename C>
inline typename C::iterator end(C& c) {
	return c.end();
}

Jak można wywnioskować z implementacji, typ kontainera C musi spełniać pewne założenia z container concept STL-a – posiadać metody begin(), end() oraz współtowarzyszące typy iteratora.

Można nieco zmodyfikować i dostarczy częściowe specjalizacje dla wszystkich typów kontenera z STL-a, ale zaciemniłoby to kod, a tak naprawdę chyba raczej nic nie wzniosło do projektu, prócz większej kontroli nad typami wejściowymi.

Wersja dla tablic oraz ta przedstawiona wyżej w zasadzie wystarczą, działają poprawnie dopóki programista jako parametr przekaże poprawny typ kontenera lub tablice. W innych przypadkach wystąpi błąd kompilacji – idealnie, po co dla błędnego użycia ma się nie wywalać kompilacja? Tak, więc, czego chcieć więcej?

Drugim, bardziej skomplikowanym (na pierwszy rzut) rozwiązaniem może być skorzystanie z metaprogramowania i boostowego MPL oraz klas cech:

template <typename T>
inline T* begin(T& t, typename boost::enable_if<boost::is_array<T> >::type* dummy = 0) {
	...
}
 
template <typename T>
inline typename T::iterator begin(T& t, typename boost::enable_if<mpu::std::is_container<T> >::type* dummy = 0) {
	...
}

O podobnych konstrukcjach będziemy opowiadać kiedyś indziej, w jednej z następnych notek poświęconych MPU lub metaprogramowaniu.

Można zastanawiać się po co w ogóle „bawić się” w takie konstrukcje. Głównym powodem jest chęć posiadania możliwości wykorzystania nowych elementów języka oraz algorytmów uogólnionych operujących na zakresach i iteratorach. A niestety nie można założyć, że wszelkie kontenery zdefiniowane przez użytkowników zawsze będą posiadać metody begin() i end(). Z tego samego założenia wyszli twórcy nowego standardu, aktualna wersja definiuje std::begin i std::end.

Wydaje mi się, że jest to bliższe „zacieśnianie więzów” miedzy kontenerami i zakresami dostępnymi w bibliotece STL, a tymi definiowanymi przez użytkownika, czy zwykłymi tablicami lub listami inicjalizującymi. Przecież obecnie for operujący na zakresach nie ma problemów z działaniem na standardowym kontenerze, czy tablicy:

int tab[5] = { 1, 2, 3, 4, 5 };
 
for (const int i : tab) {
	std::cout << i;
}

To dlaczego podobnie (prosto) nie można byłoby korzystać z gamy algorytmów uogólnionych?

Innym nasuwającym się pytaniem, może być „kiedy używać metody, a kiedy funkcji begin/end?” Według mnie, zawsze używać wersji funkcyjnej, bez problemu będziemy w stanie zmienić typ kontenera, nawet na taki który jest zwykłą tablicą, bądź nie spełniającym konceptu STL-a pojemnikiem (wystarczy wtedy przeciążyć bądź wyspecjalizować funkcje begin i end dla danego typu) bez żadnych kłopotów. Promują one jednolitość i spójność oraz pozwalają na bardziej generyczne programowanie, z czym się zgadza Herb Sutter w Elements of Modern C++ Style.

Uwaga! W niniejszej notatce przedstawiono tylko fragmenty kodu konstrukcji przyjmujących parametry jako referencje lub const-referencje, w real-world należy zadbać o obie wersje, aby się dopełniały.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *