Quão bem o Unicode é suportado no C ++ 11?

Eu li e ouvi que o C ++ 11 suporta Unicode. Algumas perguntas sobre isso:

  • Quão bem a biblioteca padrão C ++ suporta Unicode?
  • O std::string faz o que deveria?
  • Como eu uso isso?
  • Onde estão os possíveis problemas?

Quão bem a biblioteca padrão C ++ suporta unicode?

Terrivelmente

Uma varredura rápida nos resources da biblioteca que podem fornecer suporte a Unicode me fornece essa lista:

  • Biblioteca de cordas
  • Biblioteca de localização
  • Biblioteca de input / saída
  • Biblioteca de expressões regulares

Acho que todos, exceto o primeiro, fornecem apoio terrível. Voltarei a isso com mais detalhes depois de um rápido desvio através de suas outras perguntas.

O std::string faz o que deveria?

Sim. De acordo com o padrão C ++, isso é o que o std::string e seus irmãos devem fazer:

O modelo de class basic_string descreve objects que podem armazenar uma sequência que consiste em um número variável de objects arbitrários semelhantes a caracteres com o primeiro elemento da sequência na posição zero.

Bem, std::string faz isso bem. Isso fornece alguma funcionalidade específica do Unicode? Não.

Deveria? Provavelmente não. std::string é bom como uma seqüência de objects char . Isso é útil; o único incômodo é que é uma visão de baixo nível do texto e o C ++ padrão não fornece um nível mais alto.

Como eu uso isso?

Use-o como uma sequência de objects char ; fingir que é outra coisa está fadado a acabar em dor.

Onde estão os possíveis problemas?

Por todo o lugar? Vamos ver…

Biblioteca de cordas

A biblioteca de strings nos fornece basic_string , que é meramente uma sequência do que o padrão chama de “objects char-like”. Eu os chamo de unidades de código. Se você quiser uma visão de alto nível do texto, isso não é o que você está procurando. Esta é uma exibição de texto adequado 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 estreito e o mundo Unicode: c16rtomb / mbrtoc16 e c32rtomb / mbrtoc32 .

Biblioteca de localização

A biblioteca de localização ainda acredita que um desses “objects char-like” é igual a um “caractere”. Isso é obviamente bobo, e torna impossível fazer com 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 header :

 template  bool isspace (charT c, const locale& loc); template  bool isprint (charT c, const locale& loc); template  bool iscntrl (charT c, const locale& loc); // ... template  charT toupper(charT c, const locale& loc); template  charT tolower(charT c, const locale& loc); // ... 

Como você espera que qualquer uma dessas funções categorize corretamente, por exemplo, U + 1F34C ʙᴀɴᴀɴᴀ, como em u8"🍌" ou u8"\U0001F34C" ? Não há como funcionar, porque essas funções usam apenas uma unidade de código como input.

Isso poderia funcionar com uma localidade apropriada se você char32_t apenas o char32_t : U'\U0001F34C' é uma unidade de código único em UTF-32.

No entanto, isso ainda significa que você só obtém as transformações de maiúsculas simples com toupper e tolower , que, por exemplo, não são boas o suficiente para algumas localidades alemãs: “ß” uppercases para “SS” ☦ mas toupper só pode retornar uma unidade de código de caractere .

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 dada codificação em strings em outra dada codificação. Existem dois tipos de string envolvidos nessa transformação, que o padrão chama de string de bytes e string larga. Como esses termos são realmente enganosos, prefiro usar “serializado” e “desserializado”, respectivamente, em vez disso †.

As codificações para converter entre são decididas por um codecvt (uma faceta de conversão de código) passada como um argumento de tipo de modelo para wstring_convert .

wbuffer_convert executa uma function semelhante, mas como um buffer de stream desserializado de largura que encapsula um buffer de stream serializado de byte . Qualquer E / S é executada através do buffer de stream serializado byte subjacente com conversões de e para as codificações fornecidas pelo argumento codecvt. A escrita é serializada nesse buffer e, em seguida, é gravada a partir dele, e a leitura é lida no buffer e, em seguida, desserializada a partir dele.

O padrão fornece alguns modelos de class de codecvt para uso com esses resources: codecvt_utf8 , codecvt_utf16 , codecvt_utf8_utf16 e algumas especializações de codecvt . Juntas, essas facetas padrão fornecem todas as conversões a seguir. (Nota: na lista a seguir, a codificação à esquerda é sempre a string / streambuf serializada, e a codificação à direita é sempre a string / streambuf desserializada; o padrão permite conversões em ambas as direções).

  • UTF-8 ↔ UCS-2 com codecvt_utf8 e codecvt_utf8 que sizeof(wchar_t) == 2 ;
  • UTF-8 ↔ UTF-32 com codecvt_utf8 , codecvt e codecvt_utf8 que sizeof(wchar_t) == 4 ;
  • UTF-16 ↔ UCS-2 com codecvt_utf16 e codecvt_utf16 onde sizeof(wchar_t) == 2 ;
  • UTF-16 ↔ UTF-32 com codecvt_utf16 e codecvt_utf16 que sizeof(wchar_t) == 4 ;
  • UTF-8 ↔ UTF-16 com codecvt_utf8_utf16 , codecvt e codecvt_utf8_utf16 que sizeof(wchar_t) == 2 ;
  • estreito ↔ de largura com codecvt
  • no-op com codecvt .

Vários deles são úteis, mas há muitas coisas estranhas aqui.

Primeiro, santos altos substitutos! esse esquema de nomenclatura é confuso.

Então, há muito suporte a UCS-2. O UCS-2 é uma codificação do Unicode 1.0 que foi substituída em 1996 porque suporta apenas o plano multilingue básico. Por que a comissão achou desejável focar 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 freqüência aqui.

Eu diria que char16_t é obviamente destinado a armazenar unidades de código UTF-16. No entanto, esta é uma parte do padrão que pensa o contrário. codecvt_utf8 não tem nada a ver com o UTF-16. Por exemplo, wstring_convert>().to_bytes(u"\U0001F34C") irá compilar bem, mas falhará incondicionalmente: a input será tratada como a string UCS-2 u"\xD83C\xDF4C" , que não pode ser convertido 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 de um stream de bytes UTF-16 em uma string UTF-16 com essas facetas. Se você tiver uma seqüência de bytes UTF-16, não poderá desserializá-la em uma string char16_t . Isso é surpreendente, porque é mais ou menos uma conversão de identidade. Ainda mais surpreendente, no entanto, é o fato de que há suporte para desserializar de um stream UTF-16 em uma string UCS-2 com codecvt_utf16 , que é na verdade uma conversão com perdas.

O suporte a UTF-16-as-bytes é bastante bom: suporta a detecção de endianess a partir de uma BOM, ou a seleção explícita no código. Também suporta a produção de saída com e sem uma lista de materiais.

Existem algumas possibilidades de conversão mais interessantes ausentes. Não é possível desserializar de um stream ou cadeia de bytes UTF-16 para uma cadeia de caracteres UTF-8, pois o UTF-8 nunca é suportado como o formulário desserializado.

E aqui o mundo estreito / largo é completamente separado do mundo UTF / UCS. Não há conversões entre as codificações narrow / wide de estilo antigo e nenhuma codificação Unicode.

Biblioteca de input / saída

A biblioteca de E / S pode ser usada para ler e gravar texto em codificações Unicode usando as wstring_convert e wbuffer_convert descritas acima. Eu não acho que há muito mais que precisaria ser suportado por esta parte da biblioteca padrão.

Biblioteca de expressões regulares

Eu expliquei sobre problemas com regexes de C ++ e Unicode no Stack Overflow antes. Não vou repetir todos esses pontos aqui, mas apenas afirmar que os regexes de C ++ não possuem suporte Unicode de nível 1, que é o mínimo 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á muita funcionalidade Unicode que não pode ser vista 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: ICU e Boost.Locale .


† Uma string de byte é, sem surpresa, uma string de bytes, isto é, objects char . No entanto, ao contrário de um literal de cadeia larga , que é sempre uma matriz de objects wchar_t , uma “cadeia larga” neste contexto não é necessariamente uma cadeia de objects wchar_t . Na verdade, o padrão nunca define explicitamente o que uma “string larga” significa, então somos deixados para adivinhar o significado do uso. Como a terminologia padrão é desleixada e confusa, uso a minha própria, em nome da clareza.

Codificações como UTF-16 podem ser armazenadas como seqüências de char16_t , que então não possuem endianness; ou elas podem ser armazenadas como sequências de bytes, que possuem endianness (cada par de bytes consecutivos pode representar um valor char16_t diferente dependendo do endianness). O padrão suporta esses dois formulários. Uma sequência de char16_t é mais útil para manipulação interna no programa. Uma seqüência de bytes é o caminho para trocar essas cadeias com o mundo externo. Os termos que usarei em vez de “byte” e “wide” são “serializados” e “desserializados”.

‡ Se você está prestes a dizer “mas o Windows!” segure o seu 🐎🐎 . Todas as versões do Windows desde o Windows 2000 usam o UTF-16.

☦ Sim, eu sei sobre os groeses Eszett (ẞ), mas mesmo se você mudasse todas as localidades alemãs durante a noite para ter ß maiúsculas para ẞ, ainda haveria muitos outros casos em que isso falharia. Tente usar 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 maiúsculas para um J maiúsculo e um caron combinado.

O Unicode não é suportado pela Biblioteca Padrão (para qualquer significado razoável de suporte).

std::string não é melhor que std::vector : é completamente alheio ao Unicode (ou qualquer outra representação / codificação) e simplesmente trata seu conteúdo como um blob de bytes.

Se você só precisa armazenar e catenar bolhas, funciona muito bem; mas assim que você desejar a funcionalidade Unicode (número de pontos de código, número de grafemas, …) você está sem sorte.

A única biblioteca abrangente que conheço para isso é a UTI. A interface C ++ foi derivada do Java, embora esteja longe de ser idiomática.

Você pode armazenar com segurança UTF-8 em um std::string (ou em um char[] ou char* , para esse assunto), devido ao fato de que um Unicode NUL (U + 0000) é um byte nulo em UTF-8 e que esta é a única maneira que um byte nulo pode ocorrer em UTF-8. Portanto, suas strings UTF-8 serão adequadamente finalizadas de acordo com todas as funções de strings C e C ++, e você pode arrastá-las com i ++ C ++ (incluindo std::cout e std::cerr , desde que sua região seja UTF -8).

O que você não pode fazer com std::string para UTF-8 é obter o comprimento em pontos de código. std::string::size() irá dizer-lhe o comprimento da string em bytes , o que é apenas igual ao número de pontos de código quando você está dentro do subconjunto ASCII de UTF-8.

Se você precisar operar em strings UTF-8 no nível do ponto de código – não apenas armazená-las e imprimi-las – ou se você está lidando com UTF-16, que provavelmente tem muitos bytes nulos internos, você precisa para examinar os tipos de cadeia de caracteres largos.

O C ++ 11 possui alguns novos tipos de string 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 legal de obter o tamanho (em pontos de código) de uma string UTF-8.

No entanto, existe uma biblioteca bastante útil chamada tiny-utf8 , que é basicamente uma substituição para std::string / std::wstring . O objective é preencher a lacuna da class de contêineres de seqüência de caracteres utf8 ainda ausente.

Esta pode ser a maneira mais confortável de ‘lidar’ com strings utf8 (ou seja, sem normalização unicode e coisas similares). Você opera confortavelmente em pontos de código , enquanto sua string permanece codificada em caracteres codificados por comprimento de execução.