Ho letto e sentito che C ++ 11 supporta Unicode. Alcune domande al riguardo:
- In che misura la libreria standard C ++ supporta Unicode?
- Fa
std::string
quello che dovrebbe? - Come lo uso?
- Dove sono i potenziali problemi?
Ho letto e sentito che C ++ 11 supporta Unicode. Alcune domande al riguardo:
std::string
quello che dovrebbe?Risposte:
In che misura la libreria standard C ++ supporta Unicode?
Terribilmente.
Una rapida scansione delle strutture della libreria che potrebbe fornire il supporto Unicode mi dà questo elenco:
Penso che tutti tranne il primo forniscano un supporto terribile. Ci tornerò più in dettaglio dopo una rapida deviazione attraverso le altre tue domande.
Fa
std::string
quello che dovrebbe?
Sì. Secondo lo standard C ++, questo è ciò std::string
che i suoi fratelli dovrebbero fare:
Il modello di classe
basic_string
descrive oggetti che possono memorizzare una sequenza composta da un numero variabile di oggetti arbitrari simili a caratteri con il primo elemento della sequenza nella posizione zero.
Bene, std::string
va bene. Ciò fornisce funzionalità specifiche di Unicode? No.
Dovrebbe? Probabilmente no. std::string
va bene come una sequenza di char
oggetti. Questo è utile; l'unico fastidio è che si tratta di una visione di livello molto basso del testo e che il C ++ standard non ne fornisce una di livello superiore.
Come lo uso?
Usalo come una sequenza di char
oggetti; fingendo che qualcos'altro è destinato a finire nel dolore.
Dove sono i potenziali problemi?
Dappertutto? Vediamo...
Libreria di stringhe
La libreria di stringhe ci fornisce basic_string
, che è semplicemente una sequenza di ciò che lo standard chiama "oggetti char-like". Le chiamo unità di codice. Se vuoi una visione di alto livello del testo, questo non è ciò che stai cercando. Questa è una vista di testo adatta per serializzazione / deserializzazione / archiviazione.
Fornisce inoltre alcuni strumenti della libreria C che possono essere utilizzati per colmare il divario tra il mondo stretto e il mondo Unicode: c16rtomb
/ mbrtoc16
e c32rtomb
/ mbrtoc32
.
Biblioteca di localizzazione
La biblioteca di localizzazione ritiene ancora che uno di quegli "oggetti simili a caratteri" sia uguale a un "carattere". Questo è ovviamente sciocco e rende impossibile far funzionare correttamente molte cose oltre qualche piccolo sottoinsieme di Unicode come ASCII.
Considera, ad esempio, ciò che lo standard chiama "interfacce di convenienza" <locale>
nell'intestazione:
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);
// ...
Come ti aspetti che una di queste funzioni classifichi correttamente, diciamo, U + 1F34C ʙᴀɴᴀɴᴀ, come in u8"🍌"
o u8"\U0001F34C"
? Non funzionerà mai, perché quelle funzioni accettano solo un'unità di codice come input.
Questo potrebbe funzionare con un'impostazione internazionale appropriata se hai usato char32_t
solo: U'\U0001F34C'
è una singola unità di codice in UTF-32.
Tuttavia, ciò significa comunque che si ottengono solo le semplici trasformazioni di involucro con toupper
e tolower
, che, ad esempio, non sono abbastanza buone per alcune versioni tedesche: "ß" maiuscolo su "SS" ☦ ma toupper
può restituire solo un'unità di codice carattere .
Successivamente, wstring_convert
/ wbuffer_convert
e le sfaccettature di conversione del codice standard.
wstring_convert
viene usato per convertire tra le stringhe in una data codifica in stringhe in un'altra data codifica. Esistono due tipi di stringa coinvolti in questa trasformazione, che lo standard chiama una stringa di byte e una stringa ampia. Dato che questi termini sono davvero fuorvianti, preferisco usare "serializzato" e "deserializzato", invece, invece †.
Le codifiche tra cui convertire sono decise da un codecvt (un facet di conversione del codice) passato come argomento del tipo di modello wstring_convert
.
wbuffer_convert
svolge una funzione simile ma come un ampio buffer di flusso deserializzato che avvolge un buffer di flusso serializzato in byte . Qualsiasi I / O viene eseguito attraverso il buffer di flusso serializzato byte sottostante con conversioni da e verso le codifiche fornite dall'argomento codecvt. La scrittura serializza in quel buffer, quindi scrive da esso e la lettura legge nel buffer e quindi si deserializza da esso.
Lo standard prevede alcuni modelli di classe codecvt per l'uso con queste strutture: codecvt_utf8
, codecvt_utf16
, codecvt_utf8_utf16
, e alcune codecvt
specializzazioni. Insieme, queste sfaccettature standard forniscono tutte le seguenti conversioni. (Nota: nel seguente elenco, la codifica a sinistra è sempre la stringa / streambuf serializzata e la codifica a destra è sempre la stringa / streambuf deserializzata; lo standard consente le conversioni in entrambe le direzioni).
codecvt_utf8<char16_t>
e codecvt_utf8<wchar_t>
dove sizeof(wchar_t) == 2
;codecvt_utf8<char32_t>
, codecvt<char32_t, char, mbstate_t>
e codecvt_utf8<wchar_t>
dove sizeof(wchar_t) == 4
;codecvt_utf16<char16_t>
e codecvt_utf16<wchar_t>
dove sizeof(wchar_t) == 2
;codecvt_utf16<char32_t>
e codecvt_utf16<wchar_t>
dove sizeof(wchar_t) == 4
;codecvt_utf8_utf16<char16_t>
, codecvt<char16_t, char, mbstate_t>
e codecvt_utf8_utf16<wchar_t>
dove sizeof(wchar_t) == 2
;codecvt<wchar_t, char_t, mbstate_t>
codecvt<char, char, mbstate_t>
.Molti di questi sono utili, ma ci sono molte cose imbarazzanti qui.
Prima di tutto, santo surrogato! quello schema di denominazione è disordinato.
Quindi, c'è molto supporto UCS-2. UCS-2 è una codifica di Unicode 1.0 che è stata sostituita nel 1996 perché supporta solo il piano multilingue di base. Non so perché il comitato abbia ritenuto opportuno concentrarsi su una codifica sostituita più di 20 anni fa. Non è che il supporto per più codifiche sia negativo o altro, ma UCS-2 appare troppo spesso qui.
Direi che char16_t
è ovviamente pensato per la memorizzazione di unità di codice UTF-16. Tuttavia, questa è una parte dello standard che la pensa diversamente. codecvt_utf8<char16_t>
non ha nulla a che fare con UTF-16. Ad esempio, wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C")
verrà compilato correttamente, ma fallirà incondizionatamente: l'input verrà trattato come la stringa UCS-2 u"\xD83C\xDF4C"
, che non può essere convertita in UTF-8 perché UTF-8 non può codificare alcun valore nell'intervallo 0xD800-0xDFFF.
Sempre sul fronte UCS-2, non c'è modo di leggere da un flusso di byte UTF-16 in una stringa UTF-16 con queste sfaccettature. Se hai una sequenza di byte UTF-16 non puoi deserializzarla in una stringa di char16_t
. Questo è sorprendente, perché è più o meno una conversione di identità. Ancora più sorprendente, tuttavia, è il fatto che esiste il supporto per la deserializzazione da un flusso UTF-16 in una stringa UCS-2 con codecvt_utf16<char16_t>
, che in realtà è una conversione con perdita.
Il supporto UTF-16-as-bytes è abbastanza buono, tuttavia: supporta il rilevamento di endianess da una DBA o la selezione esplicita nel codice. Supporta anche la produzione di output con e senza una distinta base.
Vi sono alcune possibilità di conversione più interessanti assenti. Non è possibile deserializzare da uno stream o una stringa di byte UTF-16 in una stringa UTF-8, poiché UTF-8 non è mai supportato come modulo deserializzato.
E qui il mondo stretto / largo è completamente separato dal mondo UTF / UCS. Non ci sono conversioni tra le codifiche restrittive / ampie vecchio stile e le codifiche Unicode.
Libreria di input / output
La libreria I / O può essere utilizzata per leggere e scrivere testo nelle codifiche Unicode utilizzando le funzioni wstring_convert
e wbuffer_convert
descritte sopra. Non credo ci sia molto altro che dovrebbe essere supportato da questa parte della libreria standard.
Biblioteca di espressioni regolari
In precedenza ho spiegato i problemi con le regex C ++ e Unicode su Stack Overflow. Non ripeterò tutti questi punti qui, ma affermerò semplicemente che le regex C ++ non hanno il supporto Unicode di livello 1, che è il minimo indispensabile per renderle utilizzabili senza ricorrere all'uso di UTF-32 ovunque.
Questo è tutto?
Sì è quello. Questa è la funzionalità esistente. Esistono molte funzionalità Unicode che non si vedono da nessuna parte come algoritmi di normalizzazione o segmentazione del testo.
U + 1F4A9 . Esiste un modo per ottenere un supporto Unicode migliore in C ++?
I soliti sospetti: ICU e Boost.Locale .
† Una stringa di byte è, ovviamente, una stringa di byte, ovvero char
oggetti. Tuttavia, a differenza di una stringa larga letterale , che è sempre una matrice di wchar_t
oggetti, una "stringa larga" in questo contesto non è necessariamente una stringa di wchar_t
oggetti. In effetti, lo standard non definisce mai in modo esplicito cosa significhi una "stringa larga", quindi non ci resta che indovinare il significato dall'uso. Poiché la terminologia standard è sciatta e confusa, ne uso la mia, in nome della chiarezza.
Codifiche come UTF-16 possono essere memorizzate come sequenze di char16_t
, che quindi non hanno endianness; oppure possono essere memorizzati come sequenze di byte, che hanno endianness (ogni coppia consecutiva di byte può rappresentare un char16_t
valore diverso a seconda dell'endianness). Lo standard supporta entrambe queste forme. Una sequenza di char16_t
è più utile per la manipolazione interna nel programma. Una sequenza di byte è il modo di scambiare tali stringhe con il mondo esterno. I termini che userò al posto di "byte" e "wide" sono quindi "serializzati" e "deserializzati".
‡ Se stai per dire "ma Windows!" tieni premuto il tuo 🐎🐎 . Tutte le versioni di Windows da Windows 2000 utilizzano UTF-16.
☦ Sì, conosco il großes Eszett (ẞ), ma anche se dovessi cambiare tutti i locali tedeschi durante la notte per avere ß maiuscolo in ẞ, ci sono ancora molti altri casi in cui questo fallirebbe. Prova a mettere in maiuscolo U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ. Non esiste ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; diventa maiuscolo fino a due F. Oppure U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; non c'è capitale precomposto; diventa maiuscolo solo con una J maiuscola e un caron combinato.
Unicode non è supportato dalla Libreria standard (per qualsiasi significato ragionevole di supportato).
std::string
non è meglio di std::vector<char>
: è completamente ignaro di Unicode (o di qualsiasi altra rappresentazione / codifica) e tratta semplicemente il suo contenuto come un blocco di byte.
Se avete solo bisogno di memorizzare e catenate blob , funziona piuttosto bene; ma non appena desideri la funzionalità Unicode (numero di punti di codice , numero di grafemi ecc.) sei sfortunato.
L'unica libreria completa che conosco per questo è ICU . L'interfaccia C ++ è stata derivata da quella Java, quindi è tutt'altro che idiomatica.
È possibile archiviare in modo sicuro UTF-8 in un std::string
(o in un char[]
o char*
, del resto), a causa del fatto che un Unicode NUL (U + 0000) è un byte null in UTF-8 e che questo è l'unico modo un null byte può verificarsi in UTF-8. Pertanto, le stringhe UTF-8 verranno terminate correttamente in base a tutte le funzioni di stringa C e C ++ e sarà possibile spostarle con iostreams C ++ (incluso std::cout
e std::cerr
, purché la propria locale sia UTF-8).
Quello che non puoi fare con std::string
UTF-8 è ottenere la lunghezza in punti di codice. std::string::size()
ti dirà la lunghezza della stringa in byte , che è uguale al numero di punti di codice solo all'interno del sottoinsieme ASCII di UTF-8.
Se devi operare su stringhe UTF-8 a livello di punto di codice (cioè non solo memorizzarle e stamparle) o se hai a che fare con UTF-16, che probabilmente avrà molti byte null interni, devi esaminare i tipi di stringa di caratteri ampi.
std::string
può essere gettato in iostreams con valori null incorporati bene.
c_str()
affatto perché size()
funziona ancora. Solo le API rotte (ovvero quelle che non sono in grado di gestire valori null incorporati come la maggior parte del mondo C) si interrompono.
c_str()
perché c_str()
si suppone che restituiscano i dati come una stringa C con terminazione null --- il che è impossibile, a causa del fatto che le stringhe C non possono avere null incorporati.
c_str()
ora restituisce semplicemente lo stesso data()
, cioè tutto. Le API che assumono una dimensione possono utilizzarla. API che non lo fanno, non possono.
c_str()
assicura che il risultato sia seguito da un oggetto NUL simile a un carattere, e non credo che lo data()
faccia. No, sembra data()
che anche adesso lo faccia. (Naturalmente, questo non è necessario per le API che consumano la dimensione invece di inferirla da una ricerca terminatore)
C ++ 11 ha un paio di nuovi tipi di stringhe letterali per Unicode.
Sfortunatamente il supporto nella libreria standard per codifiche non uniformi (come UTF-8) è ancora scarso. Ad esempio, non esiste un modo carino per ottenere la lunghezza (in punti di codice) di una stringa UTF-8.
std::string
può contenere una stringa UTF-8 senza problemi, ma ad esempio il length
metodo restituisce il numero di byte nella stringa e non il numero di punti di codice.
ñ
come 'LETTER SMALL LETTER N WITH TILDE' (U + 00F1) (che è un punto di codice) o 'LATIN SMALL LETTER N' ( U + 006E) seguito da "COMBINING TILDE" (U + 0303) che è due punti di codice.
LATIN SMALL LETTER N'
== (U+006E) followed by 'COMBINING TILDE' (U+0303)
.
Tuttavia, esiste una libreria piuttosto utile chiamata tiny-utf8 , che è sostanzialmente una sostituzione drop-in per std::string
/ std::wstring
. Ha lo scopo di colmare il vuoto della classe contenitore di stringhe utf8 ancora mancante.
Questo potrebbe essere il modo più comodo di "gestire" le stringhe utf8 (cioè senza la normalizzazione unicode e cose simili). Operi comodamente su punti di codice , mentre la stringa rimane codificata in char
s con codifica di lunghezza di esecuzione .