Jaki jest sens cech postaci STL?

84

Zauważyłem, że w mojej kopii odniesienia do SGI STL jest strona o cechach postaci, ale nie widzę, jak są one używane? Czy zastępują funkcje string.h? Wydaje się, że nie są używane przez std::string, np. length()Metoda on std::stringnie korzysta z length()metody cech charakteru . Dlaczego cechy charakteru istnieją i czy są kiedykolwiek używane w praktyce?

Matthew Smith
źródło

Odpowiedzi:

172

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_stringktóry wygląda następująco:

template <typename charT, typename traits = char_traits<charT> >
    class basic_string;

Następnie std::stringdefiniuje 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 CaseInsensitiveStringtak, że mogę go mieć

CaseInsensitiveString c1 = "HI!", c2 = "hi!";
if (c1 == c2) {  // Always true
    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 comparefunkcji. Ta funkcja jest z kolei definiowana przez wywołanie

traits::compare(this->data(), str.data(), rlen)

gdzie strjest ciągiem, z którym porównujesz i rlenjest mniejszą z dwóch długości łańcucha. Jest to całkiem interesujące, ponieważ oznacza to, że w definicji compareuż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 comparetak, 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 eqi lttutaj, 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 CaseInsensitiveStringtrywialnie 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_traitsw tym typie, a następnie tworzyć ciągi z tego typu. Na przykład w interfejsie API systemu Windows istnieje typ, TCHARktó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 TCHARs, 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::stringnie używa traits::length, okazuje się, że tak jest w kilku miejscach. Przede wszystkim, gdy skonstruować std::stringSpośród char*napisu w stylu C, nowa długość łańcucha pochodzi dzwoniąc traits::lengthna tym sznurku. Wydaje się, że traits::lengthjest 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::stringjest używany do pracy z łańcuchami o dowolnej treści.

templatetypedef
źródło
14
Wygląda na to, że oddałeś sprawiedliwość swojej nazwie użytkownika :) Być może ma to również znaczenie: wiele bibliotek boost używa pojęć i klas cech typu, więc nie jest to tylko biblioteka standardowa. Ponadto podobne techniki są używane w innych językach bez użycia szablonów, patrz ezoteryczny przykład: ocaml.janestreet.com/?q=node/11 .
phooji
2
ładna struktura (trójskładnikowe drzewo wyszukiwania), jednak zwróciłbym uwagę, że próby można "skompaktować" na różne sposoby: 1 / używając zakresów znaków do wskazywania na dziecko, a nie pojedynczych znaków (zysk jest oczywisty), 2 / kompresja ścieżki (Patricia Trees) i 3 / wiadra na końcu gałęzi (tj. po prostu użyj posortowanej tablicy ciągów, o ile jest ich mniej niż K). Połączenie tych elementów (połączyłem 1 i 3) drastycznie zmniejsza zużycie pamięci bez wpływu na wydajność prędkości o więcej niż stały współczynnik (i faktycznie łyżki zmniejszają liczbę skoków).
Matthieu M.
2
@ dan04: Spróbuj uzyskać dowolną standardową klasę / algorytm, aby używać twojej funkcji.
Xeo,
2
Więc ... mówiąc w skrócie, cechy to po prostu jakiś rodzaj interfejsu używanego przez klasę basic_string do manipulowania różnymi typami znaków, niezależnie od tego, czym one naprawdę są, prawda?
Virus721,
1
@ Virus721 Cechy nie są raczej implementacjami „podłączonymi” do danej klasy? To, co klasa otrzymuje z cechy, to IMHO, implementacja, a nie interfejs. Cecha ma oczywiście swój interfejs.
dom_beau