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

183

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::stringo que deveria?
  • Como eu uso isso?
  • Onde estão os problemas em potencial?
Ralph Tandetzky
fonte
19
"Std :: string faz o que deveria?" O que você acha que deveria fazer?
R. Martinho Fernandes
2
Eu uso utfcpp.sourceforge.net para minhas necessidades de utf8. É um arquivo de cabeçalho simples que fornece iteradores para strings unicode.
Fscan #
2
std :: string deve armazenar bytes, isto é, a sequência da unidade de código da codificação UTF-8. Sim, faz exatamente isso, desde o começo. utf8everywhere.org
Pavel Radzivilovsky
3
Os maiores problemas em potencial com o suporte a Unicode estão no Unicode e seu uso na própria tecnologia da informação. O Unicode não é adequado (e não foi projetado) para o que é usado. O Unicode foi projetado para reproduzir todos os glifos possíveis que foram escritos em algum lugar por alguém, em algum momento com todas as nuances improváveis ​​e pedantes possíveis, incluindo 3 ou 4 significados diferentes e 3 ou 4 maneiras diferentes de compor o mesmo glifo. Não é para ser útil para ser usado no idioma do dia a dia, nem para ser aplicável ou para ser processado de maneira fácil ou sem ambiguidade.
Damon
11
Sim, ele foi projetado para ser usado na linguagem cotidiana. Mine pelo menos. E o seu provavelmente também. Acontece que processar texto humano de uma maneira geral é uma tarefa muito difícil. Nem sequer é possível definir sem ambiguidade o que é um personagem. A reprodução geral de glifos nem sequer faz parte do estatuto Unicode.
precisa saber é o seguinte

Respostas:

267

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:

  • Biblioteca de strings
  • Biblioteca de localização
  • Biblioteca de entrada / saída
  • Biblioteca de expressões regulares

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::stringo que deveria?

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

O modelo de classe basic_stringdescreve 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::stringisso está bem? Isso fornece alguma funcionalidade específica para Unicode? Não.

Deveria? Provavelmente não. std::stringé bom como uma sequência de charobjetos. 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 charobjetos; 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/ mbrtoc16e 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_tapenas: 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 touppere minúsculas e tolowerque, por exemplo, não são boas o suficiente para algumas localidades alemãs: "ß" maiúsculas para "SS" ☦, mas toupperpode retornar apenas uma unidade de código de caracteres .

Em seguida, wstring_convert/ wbuffer_converte 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_convertexecuta 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 codecvtespecializaçõ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).

  • UTF-8 ↔ UCS-2 com codecvt_utf8<char16_t>e codecvt_utf8<wchar_t>onde sizeof(wchar_t) == 2;
  • UTF-8 ↔ UTF-32 com codecvt_utf8<char32_t>, codecvt<char32_t, char, mbstate_t>e codecvt_utf8<wchar_t>em que sizeof(wchar_t) == 4;
  • UTF-16 ↔ UCS-2 com codecvt_utf16<char16_t>e codecvt_utf16<wchar_t>onde sizeof(wchar_t) == 2;
  • UTF-16 ↔ UTF-32 com codecvt_utf16<char32_t>e codecvt_utf16<wchar_t>onde sizeof(wchar_t) == 4;
  • UTF-8 ↔ UTF-16 com codecvt_utf8_utf16<char16_t>, codecvt<char16_t, char, mbstate_t>e codecvt_utf8_utf16<wchar_t>ondesizeof(wchar_t) == 2 ;
  • estreito ↔ largo com codecvt<wchar_t, char_t, mbstate_t>
  • no-op com 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_converte wbuffer_convertdescritos 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, charobjetos. No entanto, diferentemente de uma literal de cadeia ampla , que é sempre uma matriz de wchar_tobjetos, uma "cadeia ampla" nesse contexto não é necessariamente uma cadeia de wchar_tobjetos. 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_tvalor 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.

R. Martinho Fernandes
fonte
26
Quanto mais eu leio sobre isso, mais tenho a sensação de não entender nada sobre tudo isso. Li a maioria dessas coisas há alguns meses e ainda sinto que estou descobrindo tudo de novo ... Para simplificar para o meu pobre cérebro que agora dói um pouco, todos esses conselhos sobre o utf8 em todos os lugares ainda são válidos, certo? Se eu "apenas" quiser que meus usuários possam abrir e gravar arquivos, independentemente das configurações do sistema, posso pedir o nome do arquivo, armazená-lo em uma string std :: e tudo deve funcionar corretamente, mesmo no Windows? Desculpe para pedir que (novamente) ...
Uflex
5
@Uflex Tudo o que você realmente pode fazer com std :: string é tratá-lo como um blob binário. Em uma implementação Unicode adequada, nem a codificação interna (porque está oculta nos detalhes da implementação) nem a externa (são importantes), você ainda precisa ter o codificador / decodificador disponível).
Cat Plus Plus
3
@Uflex talvez. Não sei se seguir um conselho que você não entende é uma boa ideia.
R. Martinho Fernandes
1
Há uma proposta para suporte a Unicode no C ++ 2014/17. No entanto, é 1, talvez daqui a 4 anos e de pouca utilidade agora. open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3572.html
graham.reeds
20
@ graham.reeds haha, obrigado, mas eu sabia disso. Verifique a seção "Agradecimentos";)
R. Martinho Fernandes
40

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

std::stringnã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.

Matthieu M.
fonte
2
E o Boost.Locale ?
Uflex
11
@Uflex: na página que você vinculou Para atingir esse objetivo, o Boost.Locale usa a moderna biblioteca Unicode e Localização: ICU - International Components for Unicode.
precisa
1
Boost.Locale suporta outros servidores não-UTI, consulte aqui: boost.org/doc/libs/1_53_0/libs/locale/doc/html/...
Superfly Jon
@SuperflyJon: É verdade, mas de acordo com a mesma página, o suporte ao Unicode para os back-ends que não são da UTI é "severamente limitado".
Matthieu M.
24

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::coute std::cerr, desde que seu código de idioma seja UTF-8).

O que você não pode fazer com o std::stringUTF-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.

uckelman
fonte
3
std::stringpode ser lançado em iostreams com nulos incorporados.
R. Martinho Fernandes
3
É totalmente planejado. Não quebra 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.
R. Martinho Fernandes
1
Nulos incorporados quebram 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.
uckelman
4
Não mais. 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.
R. Martinho Fernandes
6
Com a pequena diferença que 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)
Ben Voigt
8

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.

Algum cara programador
fonte
Então, ainda precisamos usar std :: wstring para nomes de arquivos, se quisermos suportar idiomas não latinos? Porque as novas strings literais realmente não ajudam aqui como a corda geralmente vêm do usuário ...
Uflex
7
@Uflex std::stringpode conter uma string UTF-8 sem problemas, mas, por exemplo, o lengthmétodo retorna o número de bytes na string e não o número de pontos de código.
Algum programador,
8
Para ser honesto, obter o comprimento em pontos de código de uma string não tem muitos usos. O comprimento em bytes pode ser usado para pré-alocar corretamente os buffers, por exemplo.
R. Martinho Fernandes
2
O número de pontos de código em uma string UTF-8 não é um número muito interessante: pode-se escrever ñ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.
Martin Bonner apoia Monica em
Todos esses comentários sobre "você não precisa disso e não precisa disso", como "número de pontos de código sem importância" etc., me parecem um tanto desconfortáveis. Depois de escrever um analisador que supostamente analisa o código-fonte utf8, depende da especificação do analisador se considera ou não LATIN SMALL LETTER N' == (U+006E) followed by 'COMBINING TILDE' (U+0303).
BitTickler 13/08/19
4

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.

Jakob Riedle
fonte