Eu li e ouvi dizer que o C ++ 11 suporta Unicode. Algumas perguntas sobre isso:
- Quão bem a biblioteca padrão C ++ suporta Unicode?
- Faz
std::string
o que deveria? - Como eu uso isso?
- Onde estão os problemas em potencial?
Eu li e ouvi dizer que o C ++ 11 suporta Unicode. Algumas perguntas sobre isso:
std::string
o que deveria?Respostas:
Quão bem a biblioteca padrão C ++ suporta unicode?
Terrivelmente.
Uma rápida varredura nos recursos da biblioteca que podem fornecer suporte a Unicode me fornece esta lista:
Acho que todos, exceto o primeiro, fornecem um apoio terrível. Voltarei a ele com mais detalhes após um rápido desvio de suas outras perguntas.
Faz
std::string
o que deveria?
Sim. De acordo com o padrão C ++, é isso que std::string
seus irmãos devem fazer:
O modelo de classe
basic_string
descreve objetos que podem armazenar uma sequência que consiste em um número variável de objetos arbitrários do tipo char com o primeiro elemento da sequência na posição zero.
Bem, std::string
isso está bem? Isso fornece alguma funcionalidade específica para Unicode? Não.
Deveria? Provavelmente não. std::string
é bom como uma sequência de char
objetos. Isso é útil; o único incômodo é que é uma visão de texto de nível muito baixo e o C ++ padrão não fornece uma de nível superior.
Como eu uso isso?
Use-o como uma sequência de char
objetos; fingir que é outra coisa está fadado ao fim.
Onde estão os problemas em potencial?
Por todo o lugar? Vamos ver...
Biblioteca de strings
A biblioteca de strings nos fornece basic_string
, que é apenas uma sequência do que o padrão chama de "objetos do tipo char". Eu os chamo de unidades de código. Se você deseja uma visualização de texto de alto nível, não é isso que você está procurando. Esta é uma exibição de texto adequada para serialização / desserialização / armazenamento.
Ele também fornece algumas ferramentas da biblioteca C que podem ser usadas para preencher a lacuna entre o mundo restrito e o mundo Unicode: c16rtomb
/ mbrtoc16
e c32rtomb
/ mbrtoc32
.
Biblioteca de localização
A biblioteca de localização ainda acredita que um desses "objetos semelhantes a caracteres" é igual a um "caractere". É claro que isso é bobagem e torna impossível que muitas coisas funcionem corretamente além de um pequeno subconjunto de Unicode, como o ASCII.
Considere, por exemplo, o que o padrão chama de "interfaces de conveniência" no <locale>
cabeçalho:
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);
// ...
Como você espera que qualquer uma dessas funções categorize adequadamente, digamos, U + 1F34C ʙᴀɴᴀɴᴀ, como em u8"🍌"
ou u8"\U0001F34C"
? Não há como isso funcione, porque essas funções recebem apenas uma unidade de código como entrada.
Isso poderia funcionar com um código de idioma apropriado se você usasse char32_t
apenas: U'\U0001F34C'
é uma única unidade de código no UTF-32.
No entanto, isso ainda significa que você só obtém as transformações simples de maiúsculas toupper
e minúsculas e tolower
que, por exemplo, não são boas o suficiente para algumas localidades alemãs: "ß" maiúsculas para "SS" ☦, mas toupper
pode retornar apenas uma unidade de código de caracteres .
Em seguida, wstring_convert
/ wbuffer_convert
e as facetas de conversão de código padrão.
wstring_convert
é usado para converter entre strings em uma determinada codificação em strings em outra determinada codificação. Existem dois tipos de sequência envolvidos nessa transformação, que o padrão chama de sequência de bytes e uma sequência ampla. Como esses termos são realmente enganosos, prefiro usar "serializado" e "desserializado", respectivamente, em vez disso †.
As codificações a serem convertidas são decididas por um codecvt (uma faceta de conversão de código) passado como um argumento de tipo de modelo para wstring_convert
.
wbuffer_convert
executa uma função semelhante, mas como um amplo buffer de fluxo desserializado que envolve um buffer de fluxo serializado de bytes . Qualquer E / S é executada através do buffer de fluxo serializado de bytes subjacente com conversões de e para as codificações fornecidas pelo argumento codecvt. Escrever serializa nesse buffer e depois grava a partir dele, e a leitura lê no buffer e desserializa a partir dele.
A norma fornece alguns modelos de classe codecvt para uso com essas facilidades: codecvt_utf8
, codecvt_utf16
, codecvt_utf8_utf16
, e algumas codecvt
especializações. Juntas, essas facetas padrão fornecem todas as seguintes conversões. (Nota: na lista a seguir, a codificação à esquerda é sempre a sequência serializada / streambuf e a codificação à direita é sempre a sequência desserializada / streambuf; o padrão permite conversões em ambas as direções).
codecvt_utf8<char16_t>
e codecvt_utf8<wchar_t>
onde sizeof(wchar_t) == 2
;codecvt_utf8<char32_t>
, codecvt<char32_t, char, mbstate_t>
e codecvt_utf8<wchar_t>
em que sizeof(wchar_t) == 4
;codecvt_utf16<char16_t>
e codecvt_utf16<wchar_t>
onde sizeof(wchar_t) == 2
;codecvt_utf16<char32_t>
e codecvt_utf16<wchar_t>
onde sizeof(wchar_t) == 4
;codecvt_utf8_utf16<char16_t>
, codecvt<char16_t, char, mbstate_t>
e codecvt_utf8_utf16<wchar_t>
ondesizeof(wchar_t) == 2
;codecvt<wchar_t, char_t, mbstate_t>
codecvt<char, char, mbstate_t>
.Vários deles são úteis, mas há muitas coisas estranhas aqui.
Primeiro: santo alto substituto! esse esquema de nomeação é confuso.
Então, há muito suporte ao UCS-2. O UCS-2 é uma codificação do Unicode 1.0 que foi substituída em 1996 porque suporta apenas o plano multilíngue básico. Por que o comitê achou desejável se concentrar em uma codificação que foi substituída há mais de 20 anos, eu não sei ‡. Não é como se o suporte para mais codificações fosse ruim ou algo assim, mas o UCS-2 aparece com muita frequência aqui.
Eu diria que isso char16_t
é obviamente destinado ao armazenamento de unidades de código UTF-16. No entanto, essa é uma parte do padrão que pensa o contrário. codecvt_utf8<char16_t>
não tem nada a ver com UTF-16. Por exemplo, wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C")
compilará bem, mas falhará incondicionalmente: a entrada será tratada como a sequência UCS-2u"\xD83C\xDF4C"
, que não pode ser convertida em UTF-8 porque o UTF-8 não pode codificar nenhum valor no intervalo 0xD800-0xDFFF.
Ainda na frente do UCS-2, não há como ler um fluxo de bytes UTF-16 em uma string UTF-16 com essas facetas. Se você tiver uma sequência de bytes UTF-16, não poderá desserializá-la em uma sequência de char16_t
. Isso é surpreendente, porque é mais ou menos uma conversão de identidade. Ainda mais surpreendente, porém, é o fato de que há suporte para desserialização de um fluxo UTF-16 para uma cadeia UCS-2 comcodecvt_utf16<char16_t>
, o que na verdade é uma conversão com perdas.
Porém, o suporte a UTF-16-as-bytes é bastante bom: suporta a detecção de endianess de uma BOM ou a seleção explícita no código. Ele também suporta a produção de saída com e sem uma lista técnica.
Existem algumas possibilidades de conversão mais interessantes ausentes. Não há como desserializar de um fluxo ou sequência de bytes UTF-16 para uma sequência UTF-8, pois o UTF-8 nunca é suportado como a forma desserializada.
E aqui o mundo estreito / amplo é completamente separado do mundo UTF / UCS. Não há conversões entre as codificações estreitas / largas no estilo antigo e as codificações Unicode.
Biblioteca de entrada / saída
A biblioteca de E / S pode ser usada para ler e escrever texto em codificações Unicode usando os recursos wstring_convert
e wbuffer_convert
descritos acima. Eu não acho que há muito mais que precisaria ser suportado por essa parte da biblioteca padrão.
Biblioteca de expressões regulares
Eu expus problemas com regexes C ++ e Unicode no estouro de pilha antes. Não repetirei todos esses pontos aqui, mas apenas declaro que os regexes C ++ não têm suporte Unicode de nível 1, que é o mínimo necessário para torná-los utilizáveis sem recorrer ao uso de UTF-32 em todos os lugares.
É isso aí?
Sim é isso. Essa é a funcionalidade existente. Há muitas funcionalidades Unicode que não são vistas em nenhum lugar como algoritmos de normalização ou segmentação de texto.
U + 1F4A9 . Existe alguma maneira de obter um melhor suporte Unicode em C ++?
Os suspeitos do costume: UTI e Boost.Locale .
† Uma sequência de bytes é, sem surpresa, uma sequência de bytes, ou seja, char
objetos. No entanto, diferentemente de uma literal de cadeia ampla , que é sempre uma matriz de wchar_t
objetos, uma "cadeia ampla" nesse contexto não é necessariamente uma cadeia de wchar_t
objetos. De fato, o padrão nunca define explicitamente o que significa uma "cadeia ampla"; portanto, devemos adivinhar o significado do uso. Como a terminologia padrão é desleixada e confusa, eu uso a minha em nome da clareza.
Codificações como UTF-16 podem ser armazenadas como sequências de char16_t
, as quais não possuem endianness; ou eles podem ser armazenados como sequências de bytes, que possuem endianness (cada par consecutivo de bytes pode representar um char16_t
valor diferente, dependendo da endianness). O padrão suporta esses dois formulários. Uma sequência de char16_t
é mais útil para manipulação interna no programa. Uma sequência de bytes é a maneira de trocar essas strings com o mundo externo. Os termos que vou usar em vez de "byte" e "wide" são, portanto, "serializados" e "desserializados".
‡ Se você está prestes a dizer "mas Windows!" segure seu 🐎🐎 . Todas as versões do Windows desde o Windows 2000 usam UTF-16.
☦ Sim, eu conheço as gromas Eszett (ẞ), mas mesmo que você altere todos os locais alemães da noite para o dia para ter maiúsculas em ẞ, ainda existem muitos outros casos em que isso falharia. Tente digitar em maiúsculas U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ. Não há ғғ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; apenas maiúsculas para dois Fs. Ou U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; não há capital pré-composto; apenas em maiúsculas para uma maiúscula J e uma combinação de caracteres.
O Unicode não é suportado pela Biblioteca Padrão (para qualquer significado razoável de suportado).
std::string
não é melhor do que std::vector<char>
: é completamente alheio ao Unicode (ou qualquer outra representação / codificação) e simplesmente trata seu conteúdo como uma bolha de bytes.
Se você só precisa armazenar e gerar blobs , funciona muito bem; mas assim que desejar a funcionalidade Unicode (número de pontos de código , número de grafemas etc.), você estará sem sorte.
A única biblioteca abrangente que conheço é a UTI . A interface C ++ foi derivada da Java, portanto, está longe de ser idiomática.
Você pode armazenar UTF-8 com segurança em um std::string
(ou em um char[]
ou char*
, nesse caso), devido ao fato de que um NUL Unicode (U + 0000) é um byte nulo no UTF-8 e que essa é a única maneira de um nulo. byte pode ocorrer em UTF-8. Portanto, suas cadeias UTF-8 serão finalizadas corretamente de acordo com todas as funções de cadeia C e C ++, e você poderá usá-las com iostreams C ++ (incluindo std::cout
e std::cerr
, desde que seu código de idioma seja UTF-8).
O que você não pode fazer com o std::string
UTF-8 é obter comprimento em pontos de código. std::string::size()
informará o comprimento da string em bytes , que é apenas igual ao número de pontos de código quando você estiver no subconjunto ASCII do UTF-8.
Se você precisar operar em cadeias UTF-8 no nível do ponto de código (ou seja, não apenas armazená-las e imprimi-las) ou se estiver lidando com a UTF-16, que provavelmente possui muitos bytes nulos internos, é necessário examinar os tipos de cadeia de caracteres largos.
std::string
pode ser lançado em iostreams com nulos incorporados.
c_str()
, porque size()
ainda funciona. Apenas APIs quebradas (ou seja, aquelas que não conseguem lidar com nulos incorporados, como a maioria do mundo C) são interrompidas.
c_str()
porque c_str()
é suposto retornar os dados como uma cadeia C terminada em nulo - o que é impossível, devido ao fato de que as cadeias C não podem ter nulos incorporados.
c_str()
agora simplesmente retorna o mesmo que data()
, ou seja, tudo isso. APIs que assumem um tamanho podem consumi-lo. APIs que não, não podem.
c_str()
garante que o resultado seja seguido por um objeto NUL do tipo char, e acho que data()
não. Não, parece que data()
agora faz isso também. (Claro, isso não é necessário para a APIs que consomem o tamanho, em vez de inferir que a partir de uma pesquisa terminator)
O C ++ 11 possui alguns novos tipos de cadeias literais para Unicode.
Infelizmente, o suporte na biblioteca padrão para codificações não uniformes (como UTF-8) ainda é ruim. Por exemplo, não há uma maneira agradável de obter o comprimento (em pontos de código) de uma string UTF-8.
std::string
pode conter uma string UTF-8 sem problemas, mas, por exemplo, o length
método retorna o número de bytes na string e não o número de pontos de código.
ñ
como 'LETRA PEQUENA LATINA N COM TILDE' (U + 00F1) (que é um ponto de código) ou 'LETRA PEQUENA LATINA N' ( U + 006E) seguido de 'COMBINING TILDE' (U + 0303), que é dois pontos de código.
LATIN SMALL LETTER N'
== (U+006E) followed by 'COMBINING TILDE' (U+0303)
.
No entanto, existe uma biblioteca bastante útil chamada tiny-utf8 , que é basicamente um substituto para std::string
/ std::wstring
. Ele visa preencher a lacuna da classe de contêiner utf8-string ainda ausente.
Essa pode ser a maneira mais confortável de 'lidar' com strings utf8 (ou seja, sem normalização unicode e coisas semelhantes). Você opera confortavelmente em pontos de código , enquanto sua string permanece codificada em s codificados em comprimento de execução char
.