WChar, codifiche, standard e portabilità

Quanto segue potrebbe non essere qualificato come domanda SO; se è fuori dai limiti, sentitevi liberi di dirmi di andare via. La domanda qui è fondamentalmente, “Capisco correttamente lo standard C ed è questo il modo giusto per fare le cose?”

Vorrei chiedere chiarimenti, conferme e correzioni sulla mia comprensione della gestione dei caratteri in C (e quindi C ++ e C ++ 0x). Prima di tutto, un’osservazione importante:

La portabilità e la serializzazione sono concetti ortogonali.

Le cose portatili sono cose come C, unsigned int , wchar_t . Le cose serializzabili sono cose come uint32_t o UTF-8. “Portatile” significa che è ansible ricompilare la stessa fonte e ottenere un risultato di lavoro su ogni piattaforma supportata, ma la rappresentazione binaria può essere totalmente diversa (o nemmeno esistere, ad esempio il piccione TCP-over-carrier). Le cose serializzabili invece hanno sempre la stessa rappresentazione, ad esempio il file PNG che posso leggere sul mio desktop di Windows, sul mio telefono o sullo spazzolino. Le cose portatili sono interne, le cose serializzabili si occupano di I / O. Le cose portatili sono tipicamente sicure, le cose serializzabili richiedono la punteggiatura di tipo.

Quando si tratta della gestione dei caratteri in C, ci sono due gruppi di cose relativi rispettivamente alla portabilità e alla serializzazione:

    • wchar_t , setlocale() , mbsrtowcs() / wcsrtombs() : lo standard C non dice nulla su “codifiche” ; infatti, è interamente agnostico a qualsiasi proprietà di testo o codifica. Dice solo che “il tuo punto di ingresso è main(int, char**) , ottieni un tipo wchar_t che può contenere tutti i caratteri del tuo sistema, ottieni funzioni per leggere sequenze di caratteri di input e trasformarle in stringhe lavorabili e viceversa.

    • iconv() e UTF-8,16,32: Una funzione / libreria per transcodificare tra codifiche ben definite, definite e fisse. Tutte le codifiche gestite da iconv sono universalmente comprese e concordate, con un’eccezione.

    Il ponte tra il mondo portatile, encoding-agnostico di C con il suo tipo di carattere portatile wchar_t e il mondo esterno deterministico è la conversione iconv tra WCHAR-T e UTF .

    Quindi, dovrei sempre archiviare le mie stringhe internamente in un wstring codificante-agnostico, interfacciare il CRT tramite wcsrtombs() , e usare iconv() per la serializzazione? concettualmente:

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

    In pratica, ciò significa che scriverò due wrapper per piastre di caldaia per il mio punto di ingresso del programma, ad esempio per C ++:

     // Portable wmain()-wrapper #include  #include  #include  #include  std::vector parse(int argc, char * argv[]); // use mbsrtowcs etc int wmain(const std::vector args); // user starts here #if defined(_WIN32) || defined(WIN32) #include  extern "C" int main() { setlocale(LC_CTYPE, ""); int argc; wchar_t * const * const argv = CommandLineToArgvW(GetCommandLineW(), &argc); return wmain(std::vector(argv, argv + argc)); } #else extern "C" int main(int argc, char * argv[]) { setlocale(LC_CTYPE, ""); return wmain(parse(argc, argv)); } #endif // Serialization utilities #include  typedef std::basic_string U16String; typedef std::basic_string U32String; U16String toUTF16(std::wstring s); U32String toUTF32(std::wstring s); /* ... */ 

    È questo il modo giusto per scrivere un nucleo di programma idiomatico, portatile, universale, codificante-indipendente, usando solo C / C ++ standard puro, insieme a un’interfaccia I / O ben definita a UTF usando iconv? (Si noti che problemi come la normalizzazione Unicode o la sostituzione diacritica sono al di fuori dell’ambito, solo dopo aver deciso che si desidera effettivamente Unicode (al contrario di qualsiasi altro sistema di codifica che si possa desiderare) è tempo di occuparsi di quelle specifiche, ad esempio utilizzando una libreria dedicata come libicu.)

    aggiornamenti

    Seguendo molti commenti molto carini vorrei aggiungere alcune osservazioni:

    • Se la tua applicazione vuole esplicitamente occuparsi del testo Unicode, dovresti creare la parte iconv -conversion del core e usare uint32_t / char32_t -strings internamente con UCS-4.

    • Windows: mentre l’uso di stringhe di ampie dimensioni è generalmente soddisfacente, sembra che l’interazione con la console (qualsiasi console, per quella materia) sia limitata, in quanto non sembra essere supportata alcuna codifica console multi-byte sensata e mbstowcs è essenzialmente inutile ( a parte per un banale allargamento). Ricezione di argomenti a stringa larga da, ad esempio, un drop di Explorer insieme a GetCommandLineW + CommandLineToArgvW funziona (forse dovrebbe esserci un wrapper separato per Windows).

    • File system: i file system non sembrano avere alcuna nozione di codifica e prendono semplicemente qualsiasi stringa con terminazione null come nome di file. La maggior parte dei sistemi utilizza stringhe di byte, ma Windows / NTFS accetta stringhe a 16 bit. Bisogna fare attenzione quando si scoprono i file esistenti e quando si gestiscono tali dati (ad es. char16_t sequenze char16_t che non costituiscono UTF16 valido (ad es. Surrogati nudi) sono nomi di file NTFS validi). Lo standard C fopen non è in grado di aprire tutti i file NTFS, poiché non è ansible alcuna conversione che eseguirà il mapping su tutte le stringhe a 16 bit possibili. Potrebbe essere richiesto l’uso di _wfopen specifico per Windows. Come corollario, in generale non esiste una nozione ben definita di “quanti caratteri” comprendono un determinato nome di file, in quanto non vi è alcuna nozione di “carattere” in primo luogo. Caveat emptor.

    È questo il modo giusto per scrivere un nucleo di programma idiomatico, portatile, universale, codificante-encnostico usando solo C / C ++ standard puro

    No, e non c’è assolutamente modo di soddisfare tutte queste proprietà, almeno se si desidera che il programma funzioni su Windows. Su Windows, devi ignorare gli standard C e C ++ quasi ovunque e lavorare esclusivamente con wchar_t (non necessariamente internamente, ma con tutte le interfacce al sistema). Ad esempio, se inizi con

     int main(int argc, char** argv) 

    hai già perso il supporto Unicode per gli argomenti della riga di comando. Devi scrivere

     int wmain(int argc, wchar_t** argv) 

    invece, o utilizzare la funzione GetCommandLineW , nessuna delle quali è specificata nello standard C.

    Più specificamente,

    • qualsiasi programma compatibile con Unicode su Windows deve ignorare triggersmente lo standard C e C ++ per cose come argomenti della riga di comando, I / O di file e console o manipolazione di file e directory. Questo non è certamente idiomatico . Usa invece le estensioni o i wrapper Microsoft come Boost.Filesystem o Qt.
    • La portabilità è estremamente difficile da raggiungere, in particolare per il supporto Unicode. Devi davvero essere preparato affinché tutto ciò che pensi di sapere sia sbagliato. Ad esempio, è necessario considerare che i nomi file che si utilizzano per aprire i file possono essere diversi dai nomi di file effettivamente utilizzati e che due nomi di file apparentemente diversi possono rappresentare lo stesso file. Dopo aver creato due file aeb , si potrebbe finire con un singolo file c , o con due file d ed e , i cui nomi file sono diversi dai nomi dei file passati al sistema operativo. O hai bisogno di una libreria di wrapper esterna o di molti #ifdef .
    • La codifica dell’agnosticità di solito non funziona nella pratica, soprattutto se si vuole essere portabili. Devi sapere che wchar_t è un’unità di codice UTF-16 su Windows e che char è spesso (bot non sempre) un’unità di codice UTF-8 su Linux. La consapevolezza della codifica è spesso l’objective più desiderabile: assicurarsi di sapere sempre con quale codifica si lavora, o utilizzare una libreria di wrapper che le astrae.

    Penso di dover concludere che è assolutamente imansible creare un’applicazione portatile compatibile con Unicode in C o C ++, a meno che non si desideri utilizzare librerie aggiuntive e estensioni specifiche del sistema e ci si sforzi molto. Sfortunatamente, la maggior parte delle applicazioni già falliscono in compiti relativamente semplici come “scrivere caratteri greci nella console” o “supportare qualsiasi nome di file permesso dal sistema in modo corretto”, e tali attività sono solo i primi piccoli passi verso il vero supporto Unicode.

    wchar_t tipo wchar_t perché è dipendente dalla piattaforma (non “serializzabile” dalla tua definizione): UTF-16 su Windows e UTF-32 sulla maggior parte dei sistemi Unix. Invece, usa i char16_t e / o char32_t da C ++ 0x / C1x. (Se non hai un nuovo compilatore, uint16_t come uint16_t e uint32_t per ora.)

    Definire funzioni per convertire tra le funzioni UTF-8, UTF-16 e UTF-32.

    NON scrivere versioni strette / ampie di tutte le funzioni di stringa come l’API di Windows ha fatto con -A e -W. Scegli una codifica preferita da usare internamente e attenersi ad essa. Per le cose che richiedono una codifica diversa, convertirle se necessario.

    Il problema con wchar_t è che l’elaborazione del testo codifica-agnostica è troppo difficile e dovrebbe essere evitata. Se ti attacchi con “pura C” come dici tu, puoi usare tutte le funzioni w* come wcscat e gli amici, ma se vuoi fare qualcosa di più sofisticato devi wcscat nell’abisso.

    Ecco alcune cose che sono molto più difficili con wchar_t di quanto non lo siano se scegli una delle codifiche UTF:

    • Parsing Javascript: gli identificatori possono contenere determinati caratteri al di fuori del BMP (e assumiamo che ti interessi per questo tipo di correttezza).

    • HTML: come si trasforma 𐀀 in una stringa di wchar_t ?

    • Editor di testo: come trovi i limiti del wchar_t in una stringa wchar_t ?

    Se conosco la codifica di una stringa, posso esaminare direttamente i caratteri. Se non conosco la codifica, devo sperare che tutto ciò che voglio fare con una stringa sia implementato da qualche parte in una funzione di libreria. Quindi la portabilità di wchar_t è alquanto irrilevante in quanto non lo considero un tipo di dati particolarmente utile .

    I requisiti del tuo programma potrebbero essere diversi e wchar_t potrebbe wchar_t bene per te.

    Dato che iconv non è “puro standard C / C ++”, non penso che tu stia soddisfacendo le tue specifiche.

    Ci sono nuove sfaccettature di codecvt vengono con char32_t e char16_t quindi non vedo come si possa essere sbagliato finché si è coerenti e si sceglie una codifica di tipo char + + se le faccette sono qui.

    Le facet sono descritte in 22.5 [locale.stdcvt] (da n3242).


    Non capisco come questo non soddisfi almeno alcune delle tue esigenze:

     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, 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, 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 

    Quindi il tuo codice può usare ns::string , ns::char_t , LIT'A' & LIT"Hello, World!" con abbandono spericolato, senza sapere quale sia la rappresentazione di base. Quindi usa from_interfaceX(some_string) ogni volta che è necessario. Non influisce nemmeno sulle impostazioni internazionali o sui flussi. Gli helper possono essere intelligenti quanto necessario, ad esempio codecvt_utf8 può gestire “headers”, che presumo sia standardese da cose complicate come il BOM (idem codecvt_utf16 ).

    In effetti ho scritto quanto sopra per essere il più breve ansible ma vorresti davvero aiutanti come questo:

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

    che ti danno accesso ai 3 sovraccarichi per ogni membro [from|to]_bytes , accettando cose come ad esempio const char* o intervalli.