Jak dobrze Unicode jest obsługiwany w C ++ 11?

183

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?
Ralph Tandetzky
źródło
19
„Czy std :: string robi to, co powinien?” Jak myślisz, co powinno zrobić?
R. Martinho Fernandes
2
Używam utfcpp.sourceforge.net do moich potrzeb utf8. Jest to prosty plik nagłówka, który zapewnia iteratory ciągów znaków Unicode.
fscan
2
std :: string powinien przechowywać bajty, tj. sekwencję jednostek kodujących kodowanie UTF-8. Tak, robi to od samego początku. utf8everywhere.org
Pavel Radzivilovsky
3
Największe potencjalne problemy z obsługą Unicode dotyczą Unicode i jego zastosowania w samej technologii informatycznej. Unicode nie nadaje się (i nie jest zaprojektowany) do tego, do czego jest używany. Unicode ma na celu odtworzenie każdego możliwego glifu, który gdzieś został napisany przez kogoś, w dowolnym czasie z wszelkimi nieprawdopodobnymi i pedantycznymi niuansami, w tym 3 lub 4 różnymi znaczeniami i 3 lub 4 różnymi sposobami tworzenia tego samego glifu. Nie ma być przydatny do używania w codziennym języku i nie ma być przeznaczony do zastosowania ani do łatwego lub jednoznacznego przetworzenia.
Damon
11
Tak, jest przeznaczony do używania w codziennym języku. Moje przynajmniej. I najprawdopodobniej też. Okazuje się, że ogólne przetwarzanie tekstu ludzkiego jest bardzo trudnym zadaniem. Nie jest nawet możliwe jednoznaczne zdefiniowanie, czym jest postać. Ogólne odtwarzanie glifów nie jest nawet częścią karty Unicode.
Jean-Denis Muys

Odpowiedzi:

267

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ę:

  • Biblioteka ciągów
  • Biblioteka lokalizacji
  • Biblioteka wejścia / wyjścia
  • Biblioteka wyrażeń regularnych

Myślę, że wszystkie oprócz pierwszego zapewniają straszne wsparcie. Wrócę do tego bardziej szczegółowo po szybkim objeździe pozostałych pytań.

Czystd::string robi to, co powinien?

Tak. Zgodnie ze standardem C ++ to, co std::stringpowinno zrobić jego rodzeństwo:

Szablon klasy basic_stringopisuje 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::stringto w porządku? Czy zapewnia to funkcjonalność specyficzną dla Unicode? Nie.

Powinien? Prawdopodobnie nie. std::stringjest w porządku jako sekwencja charobiektó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/ mbrtoc16i 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_ttylko: U'\U0001F34C'jest pojedynczą jednostką kodu w UTF-32.

Jednak nadal oznacza to, że otrzymujesz tylko proste przekształcenia obudowy za pomocą toupperi tolower, które, na przykład, nie są wystarczające dla niektórych niemieckich ustawień regionalnych: wielkie litery „ß” na „SS” ☦, ale touppermogą zwrócić tylko jeden znak jednostkę kodu .

Dalej, wstring_convert/ wbuffer_converti standardowe aspekty konwersji kodu.

wstring_convertsł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_convertwykonuje 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 codecvtspecjalizacje. 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).

  • UTF-8 ↔ UCS-2 z codecvt_utf8<char16_t>i codecvt_utf8<wchar_t>gdziesizeof(wchar_t) == 2 ;
  • UTF-8 ↔ UTF-32 z codecvt_utf8<char32_t>, codecvt<char32_t, char, mbstate_t>i codecvt_utf8<wchar_t>gdziesizeof(wchar_t) == 4 ;
  • UTF-16 ↔ UCS-2 z codecvt_utf16<char16_t>i codecvt_utf16<wchar_t>gdziesizeof(wchar_t) == 2 ;
  • UTF-16 ↔ UTF-32 z codecvt_utf16<char32_t>i codecvt_utf16<wchar_t>gdziesizeof(wchar_t) == 4 ;
  • UTF-8 ↔ UTF-16 z codecvt_utf8_utf16<char16_t>, codecvt<char16_t, char, mbstate_t>i codecvt_utf8_utf16<wchar_t>w którym sizeof(wchar_t) == 2;
  • wąski ↔ szeroki z codecvt<wchar_t, char_t, mbstate_t>
  • brak współpracy z 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_tjest 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_converti wbuffer_convertwyposaż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_tobiektów, „szeroki łańcuch” w tym kontekście niekoniecznie jest ciągiem wchar_tobiektó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_twartość w zależności od endianowości). Standard obsługuje obie te formy. Sekwencja char16_tjest 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.

R. Martinho Fernandes
źródło
26
Im więcej o tym czytam, tym bardziej mam wrażenie, że nic nie rozumiem. Przeczytałem większość tych rzeczy kilka miesięcy temu i nadal mam wrażenie, że odkrywam to wszystko od nowa ... Aby mój prosty mózg był nieco bolesny , wszystkie te porady dotyczące utf8 wszędzie są aktualne, dobrze? Jeśli „po prostu” chcę, aby moi użytkownicy mogli otwierać i zapisywać pliki bez względu na ustawienia systemowe, mogę zapytać o nazwę pliku, zapisać go w std :: string i wszystko powinno działać poprawnie, nawet w systemie Windows? Przepraszam, że pytam (ponownie) ...
Uflex,
5
@Uflex Wszystko, co naprawdę możesz zrobić ze std :: string, to traktowanie go jako binarnego obiektu blob. W prawidłowej implementacji Unicode ani wewnętrzne (ponieważ jest ukryte głęboko w szczegółach implementacji), ani zewnętrzne kodowanie nie ma znaczenia (no cóż, nadal trzeba mieć dostępny koder / dekoder).
Cat Plus Plus
3
@Uflex może. Nie wiem, czy przestrzeganie rad, których nie rozumiesz, jest dobrym pomysłem.
R. Martinho Fernandes,
1
W C ++ 2014/17 jest propozycja obsługi Unicode. Jest to jednak 1, może 4 lata i mało użyteczne. open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3572.html
graham.reeds
20
@ graham.reeds haha, dzięki, ale zdałem sobie z tego sprawę. Sprawdź sekcję „Podziękowania”;)
R. Martinho Fernandes
40

Biblioteka Unicode nie jest obsługiwana przez Bibliotekę standardową (dla żadnego uzasadnionego znaczenia obsługiwanego).

std::stringnie 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.

Matthieu M.
źródło
2
Co powiesz na Boost.Locale ?
Uflex
11
@Uflex: ze strony, którą podłączyłeś Aby osiągnąć ten cel Boost.Locale korzysta z najnowszej biblioteki Unicode i Localization: ICU - International Components for Unicode.
Matthieu M.
1
Boost.Locale obsługuje inne backendy spoza ICU, patrz tutaj: boost.org/doc/libs/1_53_0/libs/locale/doc/html/...
Superfly Jon
@SuperflyJon: Prawda, ale zgodnie z tą samą stroną obsługa Unicode backendów spoza ICU jest „poważnie ograniczona”.
Matthieu M.
24

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::couti std::cerr, o ile twoje ustawienia regionalne to UTF-8).

To, czego nie możesz zrobić std::stringdla 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.

uckelman
źródło
3
std::stringmożna wrzucić do iostreams z osadzonymi zerami.
R. Martinho Fernandes
3
To jest całkowicie zamierzone. Nie psuje 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.).
R. Martinho Fernandes
1
Osadzone wartości zerowe ulegają zerwaniu, 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.
uckelman
4
Nigdy więcej. 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ą.
R. Martinho Fernandes
6
Z niewielką różnicą, która 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)
Ben Voigt
8

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.

Jakiś koleś programisty
źródło
Czy więc nadal musimy używać std :: wstring do nazw plików, jeśli chcemy obsługiwać języki inne niż łacińskie? Ponieważ nowe literały ciągów tak naprawdę nie pomagają, ponieważ ciąg znaków zwykle pochodzi od użytkownika ...
Uflex
7
@Uflex std::stringmoże bez problemu przechowywać ciąg UTF-8, ale np. lengthMetoda zwraca liczbę bajtów w ciągu, a nie liczbę punktów kodowych.
Jakiś programista koleś
8
Szczerze mówiąc, uzyskanie długości w punktach kodu ciągu nie ma wiele zastosowań. Długość w bajtach może być na przykład użyta do poprawnego wstępnego przydzielenia buforów.
R. Martinho Fernandes
2
Liczba punktów kodowych w łańcuchu UTF-8 nie jest bardzo interesującą liczbą: Można napisać ñ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.
Martin Bonner wspiera Monikę
Wszystkie te komentarze na temat „nie potrzebujesz tego i nie potrzebujesz tego” jak „liczba punktów kodowych nieistotnych” itp. Brzmią dla mnie nieco podejrzanie. Po napisaniu analizatora składni, który ma analizować rodzaj kodu źródłowego utf8, od specyfikacji analizatora zależy, czy weźmie on pod uwagę LATIN SMALL LETTER N' == (U+006E) followed by 'COMBINING TILDE' (U+0303).
BitTickler,
4

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.

Jakob Riedle
źródło