Cechy znaków są niezwykle ważnym składnikiem bibliotek strumieni i ciągów, ponieważ pozwalają klasom strumieni / ciągów na oddzielenie logiki przechowywanych znaków od logiki tego, jakie operacje powinny być wykonywane na tych znakach.
Po pierwsze, domyślna klasa cech znaków char_traits<T>
, jest szeroko stosowana w standardzie C ++. Na przykład nie ma wywoływanej klasy std::string
. Zamiast tego istnieje szablon klasy, std::basic_string
który wygląda następująco:
template <typename charT, typename traits = char_traits<charT> >
class basic_string;
Następnie std::string
definiuje się jako
typedef basic_string<char> string;
Podobnie standardowe strumienie są definiowane jako
template <typename charT, typename traits = char_traits<charT> >
class basic_istream;
typedef basic_istream<char> istream;
Dlaczego więc te klasy mają taką samą strukturę? Dlaczego powinniśmy używać dziwnej klasy cech jako argumentu szablonu?
Powodem jest to, że w niektórych przypadkach możemy chcieć mieć taki sam łańcuch std::string
, ale z nieco innymi właściwościami. Jednym z klasycznych przykładów jest to, że chcesz przechowywać łańcuchy w sposób ignorujący wielkość liter. Na przykład mógłbym chcieć utworzyć ciąg nazwany CaseInsensitiveString
tak, że mogę go mieć
CaseInsensitiveString c1 = "HI!", c2 = "hi!";
if (c1 == c2) {
cout << "Strings are equal." << endl;
}
Oznacza to, że mogę mieć ciąg, w którym dwa ciągi różniące się tylko rozróżnianiem wielkości liter są równe.
Załóżmy teraz, że autorzy biblioteki standardowej zaprojektowali łańcuchy bez używania cech. Oznaczałoby to, że miałbym w standardowej bibliotece niezwykle potężną klasę strun, która byłaby całkowicie bezużyteczna w mojej sytuacji. Nie mogłem ponownie użyć dużej części kodu dla tej klasy ciągów, ponieważ porównania zawsze działałyby wbrew temu, jak chciałem, aby działały. Ale używając cech, w rzeczywistości możliwe jest ponowne użycie kodu, który sterujestd::string
aby uzyskać ciąg bez rozróżniania wielkości liter.
Jeśli pobierzesz kopię standardu C ++ ISO i przyjrzysz się definicji działania operatorów porównania łańcucha, zobaczysz, że wszystkie są zdefiniowane w kategoriach compare
funkcji. Ta funkcja jest z kolei definiowana przez wywołanie
traits::compare(this->data(), str.data(), rlen)
gdzie str
jest ciągiem, z którym porównujesz i rlen
jest mniejszą z dwóch długości łańcucha. Jest to całkiem interesujące, ponieważ oznacza to, że w definicji compare
używa się bezpośrednio rozszerzeniacompare
funkcji eksportowanej przez typ cechy określony jako parametr szablonu! W konsekwencji, jeśli zdefiniujemy nową klasę cech, a następnie zdefiniujemy compare
tak, aby porównywała znaki bez uwzględniania wielkości liter, możemy zbudować klasę ciągu, która zachowuje się tak samo jakstd::string
, ale traktuje rzeczy bez rozróżniania wielkości liter!
Oto przykład. Dziedziczymy z, std::char_traits<char>
aby uzyskać domyślne zachowanie dla wszystkich funkcji, których nie piszemy:
class CaseInsensitiveTraits: public std::char_traits<char> {
public:
static bool lt (char one, char two) {
return std::tolower(one) < std::tolower(two);
}
static bool eq (char one, char two) {
return std::tolower(one) == std::tolower(two);
}
static int compare (const char* one, const char* two, size_t length) {
for (size_t i = 0; i < length; ++i) {
if (lt(one[i], two[i])) return -1;
if (lt(two[i], one[i])) return +1;
}
return 0;
}
};
(Zauważ, że zdefiniowałem także eq
i lt
tutaj, które porównują znaki równości i mniej niż, odpowiednio, a następnie zdefiniowałemcompare
w kategoriach tej funkcji).
Teraz, gdy mamy tę klasę cech, możemy CaseInsensitiveString
trywialnie zdefiniować jako
typedef std::basic_string<char, CaseInsensitiveTraits> CaseInsensitiveString;
I voila! Mamy teraz ciąg, który traktuje wszystko bez rozróżniania wielkości liter!
Oczywiście istnieją inne powody, dla których warto używać cech. Na przykład, jeśli chcesz zdefiniować ciąg, który używa jakiegoś bazowego typu znaku o stałym rozmiarze, możesz specjalizować się char_traits
w tym typie, a następnie tworzyć ciągi z tego typu. Na przykład w interfejsie API systemu Windows istnieje typ, TCHAR
który jest wąskim lub szerokim znakiem, w zależności od makr ustawionych podczas wstępnego przetwarzania. Następnie możesz utworzyć ciągi z TCHAR
s, pisząc
typedef basic_string<TCHAR> tstring;
A teraz masz ciąg TCHAR
s.
We wszystkich tych przykładach zwróć uwagę, że właśnie zdefiniowaliśmy niektóre klasy cech (lub użyliśmy jednej, która już istniała) jako parametr dla pewnego typu szablonu, aby uzyskać ciąg znaków dla tego typu. Chodzi o to, żebasic_string
autor musi tylko określić, jak używać cech, a my w magiczny sposób możemy sprawić, by używały naszych cech zamiast domyślnych, aby uzyskać ciągi, które mają pewne niuanse lub dziwactwa, które nie są częścią domyślnego typu ciągu.
Mam nadzieję że to pomoże!
EDYCJA : Jak zauważył @phooji, to pojęcie cech nie jest używane tylko przez STL, ani nie jest specyficzne dla C ++. Jako całkowicie bezwstydną autopromocję, jakiś czas temu napisałem implementację trójskładnikowego drzewa wyszukiwania (rodzaj drzewa radix opisanego tutaj ), które wykorzystuje cechy do przechowywania ciągów dowolnego typu i przy użyciu dowolnego typu porównania, którego chce klient. Może to być interesująca lektura, jeśli chcesz zobaczyć przykład, gdzie jest to stosowane w praktyce.
EDYCJA : W odpowiedzi na Twoje roszczenie, które std::string
nie używa traits::length
, okazuje się, że tak jest w kilku miejscach. Przede wszystkim, gdy skonstruować std::string
Spośród char*
napisu w stylu C, nowa długość łańcucha pochodzi dzwoniąc traits::length
na tym sznurku. Wydaje się, że traits::length
jest używany głównie do radzenia sobie z sekwencjami znaków w stylu C, które są „najmniej wspólnym mianownikiem” łańcuchów w C ++, podczas gdy std::string
jest używany do pracy z łańcuchami o dowolnej treści.