Przeczytałem i słyszałem, że C ++ 11 obsługuje Unicode. Kilka pytań na ten temat:
- Jak dobrze standardowa biblioteka C ++ obsługuje Unicode?
- Robi
std::string
robi to, co powinien? - Jak z tego korzystać?
- Gdzie są potencjalne problemy?
Przeczytałem i słyszałem, że C ++ 11 obsługuje Unicode. Kilka pytań na ten temat:
std::string
robi to, co powinien?Odpowiedzi:
Jak dobrze standardowa biblioteka C ++ obsługuje Unicode?
Niemożliwie.
Szybkie skanowanie bibliotek, które mogą zapewnić obsługę Unicode, daje mi następującą listę:
Myślę, że wszystkie oprócz pierwszego zapewniają straszne wsparcie. Wrócę do tego bardziej szczegółowo po szybkim objeździe pozostałych pytań.
Czy
std::string
robi to, co powinien?
Tak. Zgodnie ze standardem C ++ to, co std::string
powinno zrobić jego rodzeństwo:
Szablon klasy
basic_string
opisuje obiekty, które mogą przechowywać sekwencję składającą się ze zmiennej liczby dowolnych obiektów podobnych do znaków, przy czym pierwszy element sekwencji znajduje się w pozycji zero.
Czy std::string
to w porządku? Czy zapewnia to funkcjonalność specyficzną dla Unicode? Nie.
Powinien? Prawdopodobnie nie. std::string
jest w porządku jako sekwencja char
obiektów. To się przydaje; jedyną irytacją jest to, że jest to tekst na bardzo niskim poziomie, a standardowy C ++ nie zapewnia widoku na wyższym poziomie.
Jak z tego korzystać?
Użyj go jako sekwencji char
obiektów; udawanie, że to coś innego, skończy się bólem.
Gdzie są potencjalne problemy?
Wszędzie wokoło? Zobaczmy...
Biblioteka ciągów
Biblioteka ciągów zapewnia nam basic_string
sekwencję tego, co standard nazywa „obiektami podobnymi do ”. Nazywam je jednostkami kodu. Jeśli chcesz widok tekstu na wysokim poziomie, nie jest to, czego szukasz. Jest to widok tekstu odpowiedniego do serializacji / deserializacji / przechowywania.
Udostępnia również niektóre narzędzia z biblioteki C, których można użyć do wypełnienia luki między wąskim światem a światem Unicode: c16rtomb
/ mbrtoc16
i c32rtomb
/mbrtoc32
.
Biblioteka lokalizacji
Biblioteka lokalizacji nadal uważa, że jeden z tych „obiektów podobnych do znaków” jest równy jednemu „znakowi”. Jest to oczywiście głupie i uniemożliwia prawidłowe działanie wielu rzeczy poza niewielkim podzbiorem Unicode, takim jak ASCII.
Zastanówmy się na przykład, co standard nazywa „interfejsami wygody” w <locale>
nagłówku:
template <class charT> bool isspace (charT c, const locale& loc);
template <class charT> bool isprint (charT c, const locale& loc);
template <class charT> bool iscntrl (charT c, const locale& loc);
// ...
template <class charT> charT toupper(charT c, const locale& loc);
template <class charT> charT tolower(charT c, const locale& loc);
// ...
Jak spodziewasz się, że którakolwiek z tych funkcji poprawnie kategoryzuje, powiedzmy, U + 1F34C ʙᴀɴᴀɴᴀ, jak w u8"🍌"
lub u8"\U0001F34C"
? Nie ma mowy, aby to kiedykolwiek zadziałało, ponieważ te funkcje pobierają tylko jedną jednostkę kodu jako dane wejściowe.
Może to działać z odpowiednimi ustawieniami narodowymi, jeśli użyłeś char32_t
tylko: U'\U0001F34C'
jest pojedynczą jednostką kodu w UTF-32.
Jednak nadal oznacza to, że otrzymujesz tylko proste przekształcenia obudowy za pomocą toupper
i tolower
, które, na przykład, nie są wystarczające dla niektórych niemieckich ustawień regionalnych: wielkie litery „ß” na „SS” ☦, ale toupper
mogą zwrócić tylko jeden znak jednostkę kodu .
Dalej, wstring_convert
/ wbuffer_convert
i standardowe aspekty konwersji kodu.
wstring_convert
służy do konwersji między łańcuchami w danym kodowaniu na łańcuchy w innym kodowaniu. Istnieją dwa typy łańcuchów zaangażowanych w tę transformację, które standard nazywa łańcuchem bajtów i łańcuchem szerokim. Ponieważ te terminy naprawdę wprowadzają w błąd, wolę używać odpowiednio „serializacji” i „deserializacji”, odpowiednio †.
O kodowaniu, które należy przekonwertować, decyduje codecvt (aspekt konwersji kodu) przekazywany jako argument typu szablonu na wstring_convert
.
wbuffer_convert
wykonuje podobną funkcję, ale jako szeroki, niezrializowany bufor strumienia, który otacza bajtowy bufor szeregowy. Wszelkie operacje we / wy są wykonywane przez bazowy bajtowy szeregowany bufor strumienia z konwersjami do i z kodowań podanych przez argument codecvt. Zapisywanie serializuje do tego bufora, a następnie pisze z niego, a czytanie czyta do bufora, a następnie deserializuje z niego.
Norma zawiera kilka szablonów klas codecvt do użytku z tych obiektów: codecvt_utf8
, codecvt_utf16
, codecvt_utf8_utf16
, a niektóre codecvt
specjalizacje. Razem te standardowe aspekty zapewniają wszystkie następujące konwersje. (Uwaga: na poniższej liście kodowanie po lewej stronie jest zawsze serializowanym ciągiem / streambufem, a kodowanie po prawej stronie jest zawsze deserializowanym ciągiem / streambufem; standard umożliwia konwersję w obu kierunkach).
codecvt_utf8<char16_t>
i codecvt_utf8<wchar_t>
gdziesizeof(wchar_t) == 2
;codecvt_utf8<char32_t>
, codecvt<char32_t, char, mbstate_t>
i codecvt_utf8<wchar_t>
gdziesizeof(wchar_t) == 4
;codecvt_utf16<char16_t>
i codecvt_utf16<wchar_t>
gdziesizeof(wchar_t) == 2
;codecvt_utf16<char32_t>
i codecvt_utf16<wchar_t>
gdziesizeof(wchar_t) == 4
;codecvt_utf8_utf16<char16_t>
, codecvt<char16_t, char, mbstate_t>
i codecvt_utf8_utf16<wchar_t>
w którym sizeof(wchar_t) == 2
;codecvt<wchar_t, char_t, mbstate_t>
codecvt<char, char, mbstate_t>
.Kilka z nich jest przydatnych, ale tutaj jest wiele niezręcznych rzeczy.
Po pierwsze - święty, najwyższy surogacie! ten schemat nazewnictwa jest chaotyczny.
Potem jest dużo wsparcia dla UCS-2. UCS-2 to kodowanie z Unicode 1.0, które zostało zastąpione w 1996 r., Ponieważ obsługuje tylko podstawową płaszczyznę wielojęzyczną. Dlaczego komitet uznał za pożądane skoncentrowanie się na kodowaniu, które zostało zastąpione ponad 20 lat temu, nie wiem ‡. To nie jest tak, że obsługa większej liczby kodowań jest zła lub coś takiego, ale UCS-2 pojawia się tutaj zbyt często.
Powiedziałbym, że char16_t
jest to oczywiście przeznaczone do przechowywania jednostek kodu UTF-16. Jest to jednak część standardu, która uważa inaczej. codecvt_utf8<char16_t>
nie ma nic wspólnego z UTF-16. Na przykład wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C")
skompiluje się dobrze, ale bezwarunkowo zakończy się niepowodzeniem: dane wejściowe będą traktowane jako ciąg UCS-2u"\xD83C\xDF4C"
, którego nie można przekonwertować na UTF-8, ponieważ UTF-8 nie może zakodować żadnej wartości z zakresu 0xD800-0xDFFF.
Nadal na froncie UCS-2, nie ma sposobu na odczyt ze strumienia bajtów UTF-16 na ciąg UTF-16 z tymi aspektami. Jeśli masz ciąg bajtów UTF-16, nie możesz deserializować go na ciąg char16_t
. Jest to zaskakujące, ponieważ jest to mniej więcej konwersja tożsamości. Jeszcze bardziej zaskakujące jest to, że istnieje wsparcie dla deserializacji ze strumienia UTF-16 na ciąg UCS-2 codecvt_utf16<char16_t>
, co jest w rzeczywistości konwersją stratną.
Obsługa UTF-16-as-bytes jest jednak całkiem dobra: obsługuje wykrywanie endianizmu z BOM lub wybieranie go jawnie w kodzie. Obsługuje również produkcję z BOM i bez BOM.
Istnieje kilka ciekawych możliwości konwersji. Nie ma sposobu na deserializację ze strumienia bajtów UTF-16 lub łańcucha na ciąg UTF-8, ponieważ UTF-8 nigdy nie jest obsługiwany jako forma bez deserializacji.
I tutaj wąski / szeroki świat jest całkowicie oddzielony od świata UTF / UCS. Nie ma konwersji między kodowaniem wąskim / szerokim w starym stylu i kodowaniem Unicode.
Biblioteka wejścia / wyjścia
Biblioteka I / O może być używany do odczytu i zapisu tekstu w użyciu kodowania Unicode wstring_convert
i wbuffer_convert
wyposażenie opisane powyżej. Nie sądzę, aby ta część standardowej biblioteki wymagała wsparcia.
Biblioteka wyrażeń regularnych
Wcześniej wyjaśniłem problemy z wyrażeniami regularnymi C ++ i Unicode na Stack Overflow. Nie powtórzę tutaj tych wszystkich punktów, ale stwierdzę jedynie, że wyrażenia regularne w C ++ nie mają obsługi Unicode poziomu 1, co jest absolutnym minimum umożliwiającym korzystanie z nich bez konieczności korzystania z UTF-32 wszędzie.
Otóż to?
Tak, to jest to. To istniejąca funkcjonalność. Istnieje wiele funkcji Unicode, których nigdzie nie można postrzegać jako algorytmy normalizacji lub segmentacji tekstu.
U + 1F4A9 . Czy jest jakiś sposób, aby uzyskać lepszą obsługę Unicode w C ++?
Zwykli podejrzani: ICU i Boost.Locale .
† Nic dziwnego, że ciąg bajtów to ciąg bajtów, tj char
. Obiektów. Jednak w przeciwieństwie do literału szerokiego łańcucha , który zawsze jest tablicą wchar_t
obiektów, „szeroki łańcuch” w tym kontekście niekoniecznie jest ciągiem wchar_t
obiektów. W rzeczywistości standard nigdy nie definiuje wyraźnie, co oznacza „szeroki ciąg”, więc musimy odgadnąć znaczenie na podstawie użycia. Ponieważ standardowa terminologia jest niechlujna i myląca, używam własnej, w imię przejrzystości.
Kodowania takie jak UTF-16 mogą być przechowywane jako sekwencje char16_t
, które następnie nie mają endianizmu; lub mogą być przechowywane jako sekwencje bajtów, które mają endianowość (każda kolejna para bajtów może reprezentować inną char16_t
wartość w zależności od endianowości). Standard obsługuje obie te formy. Sekwencja char16_t
jest bardziej przydatna do wewnętrznych manipulacji w programie. Sekwencja bajtów jest sposobem na wymianę takich ciągów ze światem zewnętrznym. Terminy, których użyję zamiast „bajtów” i „szerokich” są zatem „serializowane” i „deserializowane”.
‡ Jeśli masz zamiar powiedzieć „ale Windows!” przytrzymaj swój 🐎🐎 . Wszystkie wersje systemu Windows od Windows 2000 używają UTF-16.
☦ Tak, wiem o großes Eszett (ẞ), ale nawet jeśli z dnia na dzień zmienisz wszystkie niemieckie lokalizacje, aby mieć ß wielkie litery na ẞ, wciąż istnieje wiele innych przypadków, w których to się nie udałoby. Spróbuj górnej obudowy U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ. Nie ma ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; to po prostu wielkie litery do dwóch Fs. Lub U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; nie ma kapitału z góry; to po prostu wielkie litery do wielkiej litery J i połączonego karonu.
Biblioteka Unicode nie jest obsługiwana przez Bibliotekę standardową (dla żadnego uzasadnionego znaczenia obsługiwanego).
std::string
nie jest lepszy niż std::vector<char>
: jest całkowicie nieświadomy Unicode (lub jakiejkolwiek innej reprezentacji / kodowania) i po prostu traktuje jego zawartość jako kroplę bajtów.
Jeśli potrzebujesz tylko przechowywać i catenate obiekty BLOB , działa całkiem dobrze; ale gdy tylko zechcesz korzystać z funkcji Unicode (liczba punktów kodowych , liczba grafemów itp.), nie masz szczęścia.
Jedyną wszechstronną biblioteką, o której wiem, jest OIOM . Interfejs C ++ wywodzi się z interfejsu Java, więc nie jest to idiomatyczne.
Można bezpiecznie przechowywać UTF-8 w sposób std::string
(albo w char[]
lub char*
, dla tej sprawy), ze względu na fakt, że NUL Unicode (U + 0000) jest bajt zerowy w UTF-8 i że jest to jedyny sposób null bajt może wystąpić w UTF-8. Dlatego twoje łańcuchy UTF-8 zostaną poprawnie zakończone zgodnie ze wszystkimi funkcjami łańcuchowymi C i C ++, i możesz je przewijać za pomocą iostreamów C ++ (w tym std::cout
i std::cerr
, o ile twoje ustawienia regionalne to UTF-8).
To, czego nie możesz zrobić std::string
dla UTF-8, to uzyskanie długości w punktach kodowych. std::string::size()
powie ci długość łańcucha w bajtach , która jest równa tylko liczbie punktów kodowych, gdy jesteś w podzbiorze ASCII UTF-8.
Jeśli potrzebujesz operować na łańcuchach UTF-8 na poziomie punktu kodowego (tj. Nie tylko przechowywać i drukować) lub jeśli masz do czynienia z UTF-16, który prawdopodobnie ma wiele wewnętrznych bajtów zerowych, musisz spojrzeć na typy ciągów znaków szerokich.
std::string
można wrzucić do iostreams z osadzonymi zerami.
c_str()
się wcale, ponieważ size()
nadal działa. Wyłączone są tylko uszkodzone interfejsy API (tj. Takie, które nie mogą obsługiwać osadzonych wartości zerowych, jak większość świata C.).
c_str()
ponieważ c_str()
powinny zwracać dane jako zakończony znakiem null ciąg C --- co jest niemożliwe, ponieważ łańcuchy C nie mogą mieć osadzonych wartości null.
c_str()
teraz po prostu zwraca to samo co data()
, tj. całość. Interfejsy API, które przyjmują rozmiar, mogą go zużywać. Interfejsy API, które nie, nie mogą.
c_str()
zapewnia, że po wyniku występuje obiekt typu NUL char, a nie sądzę, że data()
tak. Nie, wygląda na to, że data()
teraz to robi. (Oczywiście nie jest to konieczne w przypadku interfejsów API, które zużywają rozmiar, zamiast wnioskować o nim przy wyszukiwaniu terminatora)
C ++ 11 ma kilka nowych literalnych ciągów znaków dla Unicode.
Niestety wsparcie w standardowej bibliotece dla niejednorodnych kodowań (takich jak UTF-8) jest nadal złe. Na przykład nie ma dobrego sposobu na uzyskanie długości (w punktach kodowych) ciągu UTF-8.
std::string
może bez problemu przechowywać ciąg UTF-8, ale np. length
Metoda zwraca liczbę bajtów w ciągu, a nie liczbę punktów kodowych.
ñ
jako „LATIN SMALL LETTER N WITH TILDE” (U + 00F1) (który jest jednym punktem kodowym) lub „LATIN SMALL LETTER N” ( U + 006E), a następnie „ŁĄCZĄCY PŁYTKĘ” (U + 0303), która jest dwoma punktami kodowymi.
LATIN SMALL LETTER N'
== (U+006E) followed by 'COMBINING TILDE' (U+0303)
.
Jednak jest to dość przydatna biblioteka nazywa malutki-utf8 , który jest w zasadzie zamiennik dla std::string
/ std::wstring
. Ma na celu wypełnienie luki wciąż brakującej klasy kontenera utf8-string.
Może to być najwygodniejszy sposób „radzenia sobie” z ciągami utf8 (to znaczy bez normalizacji Unicode i podobnych rzeczy). Możesz wygodnie operować na punktach kodowych , podczas gdy łańcuch pozostaje zakodowany w ciągach kodowanych długością char
.