Bezpieczne iteratory w VC++

tech • 495 słów • 3 minuty czytania

Od wersji 2005 Visual C++ posiada opcję sprawdzania ważności iteratorów. Może to być przydatna funkcjonalność, która powoli uchronić się przed typowymi błędami związanymi z niepoprawnym użyciem iteratorów. O szczegółach działania można poczytać na stronach MSDN w kilku ciekawych artykułach: Checked Iterators, Debug Iterator Support.

W uproszczeniu mówiąc, przy włączonym Debug Iterator, program w wersji debug poczęstuje nas assertami, kiedy spróbujemy wykonać jakąś operację na niepoprawnym (unieważnionym1) iteratorze lub funkcji go wykorzystującej. Checked iterators za to sprawdza zakresy i wyłapuje związane z tym błędy w czasie działania programu.

Do kontroli zachowania tych mechanizmów służą makra _SECURE_SCL i _HAS_ITERATOR_DEBUGGING. Należy pamiętać, że domyślnie _SECURE_SCL jest także włączone w wersji release, przez co może prowadzić do zmniejszenia wydajności i szybkości. Oczywiście definicja makr z wartością 0 spowoduje wyłączenie tych niestandardowych zachowań, ale wtedy mogą pojawić się problemy przy linkowaniu z innymi bibliotekami, które zbudowano z odmiennymi ustawieniami tych mechanizmów - prawdopodobnie przez użycie innej wersji CRT.

Taki problem mnie złapał przy kompilowaniu programu z wyłączonym sprawdzaniem i linkowaniem do dynamicznej biblioteki z domyślnymi ustawieniami. Nagle debuger łapał dziwny wyjątek w dziwnym miejscu, w trywialnym i poprawnym kodzie. Z drugiej strony mechanizm ten pomógł mi znaleźć kilka możliwych błędów na operacjach z iteratorami, których prawdopodobnie nigdy bym się nie spodziewał.

To fakt, że czasem robił małe zamieszanie. Na przykład próba porównania unieważnionego iteratora z innym była już niemożliwa w wersji debug. Wykorzystując fakt, że vector trzyma dane w jednym spójnym kawałku pamięci, mogłem łatwo sprawdzić czy dany iterator stracił ważność - wskazywał poza zakres. A co by się stało jakby trafiał w dobry zakres? - wskazywałby na zupełnie inny element doprowadzając do niezdefiniowanego działania programu.

Innym nieudogodnieniem na jaki trafiłem, było pobranie adresu elementu na jaki wskazuje iterator. Próba dereferencji na ostatnim iteratorze (end) była sygnalizowana assertem z informacją o próbie dereferencji niepoprawnego iteratora.

Wskaźniki potrzebowałem tylko do celów traceowania i logowania przydatnych informacji w czasie debugowania i testowania. Oczywiście ktoś zaraz powie czemu nie wykorzystywałem indeksów w logach, było by czytelniej i nie sprawiłyby takich problemów. Mi się jakoś wskaźniki w tym przypadku wydawały lepsze, bo m.in. wykorzystywane są na zewnątrz poprzez API jako “uchwyty”.

Żeby uniknąć problemu z dereferencją iteratora wskazującego na end przy pobieraniu tylko i wyłącznie wskaźnika do celów logowania i prezentacji liczbowej, musiałem nieco sięgnąć do bebechów implementacyjnych STL-a używanego przez VC. Celem było dostanie się bezpośrednio do wartości wskaźnika przechowanego przez iterator.

Poniższa prosta “nakładka” pozwala na bezproblemowe pobranie wskaźnika do elementu, bez utraty przenośności:

template<typename T>
inline typename T::pointer Itr2Ptr(T iter) {
#if _MSC_VER >= 1400 && (_HAS_ITERATOR_DEBUGGING || _SECURE_SCL)
	return iter._Myptr;
#else
	return &*iter;
#endif
}

Na koniec, mała acz istotna uwaga, gdyby ktoś o tym zapomniał.
Iterator end nie wskazuje na ostatni element kontenera tylko na następny za nim lub inną “umowną” wartość zależną od implementacji. Dereferencja, pobranie wskaźnika i próba operowania na takim obiekcie jest skazana na niepowodzenie!


Przypisy

  1. Pewne operacje na iteratorach i kontenerach powodują unieważnienie istniejących iteratorów, np. dla vector-a będzie to dowolna operacja doprowadzająca do relokacji danych. ↩︎

Komentarze (0)

Dodaj komentarz

/dozwolony markdown/

/nie zostanie opublikowany/