WChars, kodowanie, standardy i przenośność

Poniższe pytania mogą nie kwalifikować się jako pytanie typu SO; jeśli jest to poza granicami, proszę, powiedz mi, żebym sobie poszedł. Pytanie tutaj brzmi zasadniczo: "czy dobrze rozumiem standard C i czy jest to właściwy sposób, aby przejść do rzeczy?"

Chciałbym prosić o wyjaśnienie, potwierdzenie i poprawki w moim zrozumieniu obsługi znaków w C (a więc C++ i C++0x). Po pierwsze, ważna uwaga:

przenośność i serializacja są pojęciami ortogonalnymi.

Przenośne rzeczy to rzeczy jak C, unsigned int, wchar_t. Serializowalne rzeczy to rzeczy takie jak uint32_t lub UTF-8. "Przenośny" oznacza, że można przekompilować to samo źródło i uzyskać wynik pracy na każdej obsługiwanej platformie, ale reprezentacja binarna może być zupełnie inna (lub nawet nie istnieje, np. TCP-over-carrier pigeon). Serializowalne rzeczy z drugiej strony zawsze mają tę samą reprezentację, np. plik PNG, który mogę odczytać na moim systemie Windows na pulpicie, w telefonie lub na szczoteczce do zębów. Rzeczy przenośne są wewnętrzne, rzeczy serializowalne zajmują się we/wy. rzeczy przenośne są typami, rzeczy serializowalne wymagają typu.

Jeśli chodzi o obsługę znaków w języku C, istnieją dwie grupy rzeczy związane odpowiednio z przenośnością i serializacją:]}
  • wchar_t, setlocale(), mbsrtowcs()/wcsrtombs(): standard C nie mówi nic o"kodowaniu" ; w rzeczywistości jest całkowicie agnostyczny dla dowolnego tekstu lub kodowania właściwości. To tylko mówi " twój punkt wejścia jest main(int, char**); otrzymujesz typ wchar_t, który może pomieścić wszystkie znaki Twojego systemu; dostajesz funkcje do odczytu wejściowych sekwencji znaków i przekształcania ich w wykonalne stringi i vice versa.

  • iconv() i UTF-8,16,32: funkcja / biblioteka do transkodowania między dobrze zdefiniowanymi, określonymi, stałymi kodowaniami. Wszystkie kodowania obsługiwane przez iconv są powszechnie zrozumiałe i uzgodnione, z jednym wyjątkiem.

Most między przenośny, kodujący-agnostyczny świat C z jego przenośnym typem znakowym wchar_t i deterministycznym światem zewnętrznym jest konwersja iconv pomiędzy WCHAR-T i UTF .

Więc, czy zawsze powinienem przechowywać moje ciągi wewnętrznie w kodowaniu-agnostic wstring, interfejs z CRT poprzez wcsrtombs(), i używać iconv() do serializacji? Koncepcyjnie:

                        my program
    <-- wcstombs ---  /==============\   --- iconv(UTF8, WCHAR_T) -->
CRT                   |   wchar_t[]  |                                <Disk>
    --- mbstowcs -->  \==============/   <-- iconv(WCHAR_T, UTF8) ---
                            |
                            +-- iconv(WCHAR_T, UCS-4) --+
                                                        |
       ... <--- (adv. Unicode malarkey) ----- libicu ---+

Praktycznie oznacza to, że napisałbym dwie okładki dla mojego punktu wejścia do programu, np. dla C++:

// Portable wmain()-wrapper
#include <clocale>
#include <cwchar>
#include <string>
#include <vector>

std::vector<std::wstring> parse(int argc, char * argv[]); // use mbsrtowcs etc

int wmain(const std::vector<std::wstring> args); // user starts here

#if defined(_WIN32) || defined(WIN32)
#include <windows.h>
extern "C" int main()
{
  setlocale(LC_CTYPE, "");
  int argc;
  wchar_t * const * const argv = CommandLineToArgvW(GetCommandLineW(), &argc);
  return wmain(std::vector<std::wstring>(argv, argv + argc));
}
#else
extern "C" int main(int argc, char * argv[])
{
  setlocale(LC_CTYPE, "");
  return wmain(parse(argc, argv));
}
#endif
// Serialization utilities

#include <iconv.h>

typedef std::basic_string<uint16_t> U16String;
typedef std::basic_string<uint32_t> U32String;

U16String toUTF16(std::wstring s);
U32String toUTF32(std::wstring s);

/* ... */

Jest to jest właściwy sposób na napisanie idiomatycznego, przenośnego, uniwersalnego, niezwiązanego z kodowaniem rdzenia programu przy użyciu tylko czystego standardu C / C++, wraz z dobrze zdefiniowanym interfejsem I / O do UTF przy użyciu iconv? (Zwróć uwagę, że kwestie takie jak normalizacja Unicode lub zamiana znaków diakrytycznych są poza zakresem; dopiero gdy zdecydujesz, że chcesz Unicode (W przeciwieństwie do jakiegokolwiek innego systemu kodowania, który możesz sobie wyobrazić), nadszedł czas, aby poradzić sobie z tymi specyfikacjami, np. używając dedykowanej biblioteki, takiej jak libicu.)

Aktualizacje

Po wielu bardzo miłych komentarzach chciałbym dodać kilka uwag:

  • Jeśli aplikacja wyraźnie chce radzić sobie z tekstem Unicode, należy zrobić iconv - conversion część rdzenia i użyć uint32_t/char32_t-struny wewnętrzne z UCS-4.

  • Windows: chociaż używanie szerokich ciągów jest ogólnie w porządku, wydaje się, że interakcja z konsolą (dowolną konsolą, jeśli o to chodzi) jest ograniczona, ponieważ nie wydaje się być wsparciem dla żadnego sensownego kodowania wielobajtowej konsoli i mbstowcs jest zasadniczo bezużyteczny (inny niż dla trywialnego rozszerzenia). Otrzymywanie szerokich argumentów z, powiedzmy, Explorer-drop razem z GetCommandLineW+CommandLineToArgvW działa (być może powinno być osobne opakowanie Dla Windows).

  • Systemy plików: systemy plików nie wydają się mieć pojęcia kodowania i po prostu przyjmują dowolny łańcuch zakończony znakiem null jako nazwę pliku. Większość systemów przyjmuje ciągi bajtów, ale Windows / NTFS zajmuje 16-bitowe struny. Należy zachować ostrożność podczas odkrywania, które Pliki istnieją i podczas przetwarzania tych danych (np. sekwencje char16_t, które nie stanowią poprawnego UTF16 (np. nagie zastępcze) są poprawnymi nazwami plików NTFS). Standard C fopen nie jest w stanie otworzyć wszystkich plików NTFS, ponieważ nie ma możliwej konwersji, która mapuje wszystkie możliwe 16-bitowe ciągi. Może być wymagane użycie specyficznego dla systemu Windows _wfopen. Jako następstwo, ogólnie nie ma dobrze zdefiniowanego pojęcia " ile znaków" zawierać nazwę danego pliku, ponieważ nie ma pojęcia "znak" w pierwszej kolejności. Caveat emptor.

Author: Kerrek SB, 2011-06-10

4 answers

Czy jest to właściwy sposób na napisanie idiomatycznego, przenośnego, uniwersalnego, kodującego-agnostycznego rdzenia programu przy użyciu tylko czystego standardu C / C++

Nie, i nie ma możliwości, aby spełnić wszystkie te właściwości, przynajmniej jeśli chcesz, aby twój program działał w systemie Windows. W systemie Windows niemal wszędzie trzeba ignorować standardy C i C++ i pracować wyłącznie z wchar_t (niekoniecznie wewnętrznie, ale we wszystkich interfejsach do systemu). Na przykład, jeśli zaczniesz z

int main(int argc, char** argv)

Straciłeś już wsparcie dla Unicode dla argumentów wiersza poleceń. Musisz napisać

int wmain(int argc, wchar_t** argv)

Zamiast tego, lub użyj funkcji GetCommandLineW, z których żadna nie jest określona w standardzie C.

Dokładniej,

  • każdy program obsługujący Unicode w systemie Windows musi aktywnie ignorować standard C i C++ dla takich rzeczy jak argumenty linii poleceń, We/Wy plików i konsoli lub manipulacja plikami i katalogami. To z pewnością nie jest idiomatyczne . Użyj Microsoft extensions lub wrappers jak Boost.System plików lub Qt.
  • przenośność jest niezwykle trudna do osiągnięcia, szczególnie w przypadku obsługi Unicode. Naprawdę musisz być przygotowany, że wszystko, co myślisz, że wiesz, jest prawdopodobnie złe. Na przykład, należy wziąć pod uwagę, że nazwy plików, których używasz do otwierania plików, mogą różnić się od nazw plików, które są faktycznie używane, i że dwie pozornie różne nazwy plików mogą reprezentować ten sam plik. Po utworzeniu dwóch plików a i b , możesz skończyć z pojedynczym plikiem c, lub dwoma plikami di e, których nazwy plików różnią się od nazw przekazanych do systemu operacyjnego. W tym celu należy utworzyć bibliotekę wrapperów zewnętrznych lub Wiele #ifdefs.
  • kodowanie agnosti zazwyczaj nie działa w praktyce, szczególnie jeśli chcesz być przenośny. Musisz wiedzieć, że wchar_t jest jednostką kodu UTF-16 w systemie Windows i że {[6] } jest często (nie zawsze) UTF-8 Jednostka kodu na Linuksie. Świadomość kodowania jest często bardziej pożądanym celem: upewnij się, że zawsze wiesz, z którym kodowaniem pracujesz, lub użyj biblioteki wrapperów, która je usuwa.

Myślę, że muszę stwierdzić, że jest całkowicie niemożliwe, aby zbudować przenośną aplikację obsługującą Unicode w C lub c++, chyba że jesteś gotów użyć dodatkowych bibliotek i rozszerzeń specyficznych dla systemu, i włożyć w to wiele wysiłku. Niestety większość aplikacji już zawodzi w stosunkowo proste zadania, takie jak "pisanie greckich znaków do konsoli" lub "Obsługa poprawnej nazwy pliku dozwolonej przez system", a takie zadania są tylko pierwszymi małymi krokami w kierunku prawdziwej obsługi Unicode.

 22
Author: Philipp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-11 21:18:07

Uniknąłbym typu wchar_t, ponieważ jest zależny od platformy (nie "serializowalny" według twojej definicji): UTF-16 W Windows i UTF-32 w większości systemów uniksopodobnych. Zamiast tego użyj typów char16_t i / lub char32_t z C++0x / C1x. (jeśli nie masz nowego kompilatora, wpisz je na razie jako uint16_t i uint32_t.)

DO definiuje funkcje do konwersji między funkcjami UTF-8, UTF-16 i UTF-32.

Nie zapisuj przeciążonych wąskich / szerokich wersji każdego ciągu funkcja podobna do Windows API z -a i-W. Wybierz jedno preferowane kodowanie do użycia wewnętrznie i trzymaj się go. W przypadku rzeczy, które wymagają innego kodowania, konwersja w razie potrzeby.

 9
Author: dan04,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-10 01:03:18

Problem z wchar_t polega na tym, że kodowanie-agnostyczne przetwarzanie tekstu jest zbyt trudne i należy go unikać. Jeśli trzymasz się "czystego C", jak mówisz, możesz użyć wszystkich funkcji w*, takich jak wcscat i przyjaciele, ale jeśli chcesz zrobić coś bardziej wyrafinowanego, musisz zanurzyć się w otchłań.

Oto kilka rzeczy, które są o wiele trudniejsze z wchar_t niż są, jeśli wybierzesz jedno z kodowań UTF:

  • Parsowanie Javascript: identyfikatory mogą zawierać pewne znaki spoza BMP (i załóżmy, że zależy ci na tego rodzaju poprawności).

  • HTML: jak zamienić &#65536; w ciąg wchar_t?

  • Edytor tekstu: jak znaleźć granice klastra grapheme w łańcuchu wchar_t?

Jeśli znam kodowanie ciągu znaków, mogę sprawdzić znaki bezpośrednio. Jeśli nie znam kodowania, muszę mieć nadzieję, że cokolwiek chcę zrobić z ciągiem znaków, zostanie zaimplementowane gdzieś przez funkcję biblioteczną. Tak więc przenośność wchar_t jest nieco nieistotna, ponieważ nie uważam jej za szczególnie przydatny typ danych .

Twoje wymagania programowe mogą się różnić i wchar_t mogą działać dobrze dla Ciebie.

 8
Author: Dietrich Epp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-11 11:35:05

Biorąc pod uwagę, że iconv nie jest " czystym standardem C / C++", myślę, że nie spełniasz własnych specyfikacji.

Pojawiają się nowe codecvt aspekty z char32_t i char16_t, więc nie rozumiem, jak możesz się mylić, o ile jesteś spójny i wybierz jeden typ znaków + kodowanie, jeśli aspekty są tutaj.

Aspekty są opisane w 22.5 [locale.stdcvt] (od n3242).


Nie rozumiem, jak to nie zaspokoi przynajmniej niektórych Twoich wymagania:

namespace ns {

typedef char32_t char_t;
using std::u32string;

// or use user-defined literal
#define LIT u32

// Communicate with interface0, which wants utf-8

// This type doesn't need to be public at all; I just refactored it.
typedef std::wstring_convert<std::codecvt_utf8<char_T>, char_T> converter0;

inline std::string
to_interface0(string const& s)
{
    return converter0().to_bytes(s);
}

inline string
from_interface0(std::string const& s)
{
    return converter0().from_bytes(s);
}

// Communitate with interface1, which wants utf-16

// Doesn't have to be public either
typedef std::wstring_convert<std::codecvt_utf16<char_T>, char_T> converter1;

inline std::wstring
to_interface0(string const& s)
{
    return converter1().to_bytes(s);
}

inline string
from_interface0(std::wstring const& s)
{
    return converter1().from_bytes(s);
}

} // ns

Wtedy twój kod może użyć ns::string, ns::char_t, LIT'A' & LIT"Hello, World!" z lekkomyślnym porzuceniem, nie wiedząc, co jest podstawą reprezentacji. Następnie użyj from_interfaceX(some_string), gdy jest to potrzebne. Nie ma to wpływu na globalne ustawienia regionalne ani strumienie. Pomocnicy mogą być tak sprytni, jak to konieczne, np. codecvt_utf8 mogą radzić sobie z "nagłówkami", które zakładam, że są standardowe od trudnych rzeczy, takich jak BOM (ditto codecvt_utf16).

W rzeczywistości napisałem powyżej, aby być jak najkrótszym, ale naprawdę chcesz helpers like this:

template<typename... T>
inline ns::string
ns::from_interface0(T&&... t)
{
    return converter0().from_bytes(std::forward<T>(t)...);
}

, które dają dostęp do 3 przeciążeń dla każdego członka [from|to]_bytes, akceptując rzeczy takie jak np. const char* lub zakresy.

 6
Author: Luc Danton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-11 10:19:55