¿Qué tan bien se admite Unicode en C ++ 11?

He leído y escuché que C ++ 11 es compatible con Unicode. Algunas preguntas sobre eso:

  • ¿Qué tan bien soporta la biblioteca estándar de C ++ Unicode?
  • ¿ std::string hace lo que debería?
  • ¿Como lo uso?
  • ¿Dónde hay problemas potenciales?

¿Qué tan bien soporta la biblioteca estándar de C ++ unicode?

Terriblemente.

Un escaneo rápido a través de las instalaciones de la biblioteca que podría proporcionar soporte Unicode me da esta lista:

  • Biblioteca de cuerdas
  • Biblioteca de localización
  • Biblioteca de entrada / salida
  • Biblioteca de expresiones regulares

Creo que todos, menos el primero, brindan un apoyo terrible. Volveré a tratarlo con más detalle después de un desvío rápido a través de sus otras preguntas.

¿ std::string hace lo que debería?

Sí. Según el estándar de C ++, esto es lo que std::string y sus hermanos deberían hacer:

La plantilla de clase basic_string describe objetos que pueden almacenar una secuencia que consiste en un número variable de objetos arbitrarios con el primer elemento de la secuencia en la posición cero.

Bueno, std::string hace eso bien. ¿Eso proporciona alguna funcionalidad específica de Unicode? No.

¿Deberia? Probablemente no. std::string está bien como una secuencia de objetos char . Eso es útil; la única molestia es que es una vista de texto de muy bajo nivel y C ++ estándar no proporciona una de mayor nivel.

¿Como lo uso?

Úselo como una secuencia de objetos char ; pretender que es otra cosa está destinado a terminar en dolor.

¿Dónde hay problemas potenciales?

¿Por todo el lugar? Veamos…

Biblioteca de cuerdas

La biblioteca de cadenas nos proporciona basic_string , que no es más que una secuencia de lo que el estándar llama “objetos de tipo char”. Los llamo unidades de código. Si desea una vista de texto de alto nivel, esto no es lo que está buscando. Esta es una vista de texto adecuada para la serialización / deserialización / almacenamiento.

También proporciona algunas herramientas de la biblioteca C que se pueden utilizar para cerrar la brecha entre el mundo angosto y el mundo Unicode: c16rtomb / mbrtoc16 y c32rtomb / mbrtoc32 .

Biblioteca de localización

La biblioteca de localización todavía cree que uno de esos “objetos similares a un carbón” es igual a un “personaje”. Esto es, por supuesto, una tontería, y hace que sea imposible obtener muchas cosas funcionando correctamente más allá de un pequeño subconjunto de Unicode como ASCII.

Considere, por ejemplo, lo que el estándar llama “interfaces de conveniencia” en el encabezado :

 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); // ... 

¿Cómo espera que alguna de estas funciones categorice correctamente, digamos, U + 1F34C ʙᴀɴᴀɴᴀ, como en u8"🍌" o u8"\U0001F34C" ? No hay forma de que funcione alguna vez, porque esas funciones toman solo una unidad de código como entrada.

Esto podría funcionar con una configuración regional adecuada si usó solo char32_t : U'\U0001F34C' es una unidad de código único en UTF-32.

Sin embargo, eso significa que solo obtendrá las transformaciones de carcasa simples con toupper y tolower , que, por ejemplo, no son lo suficientemente buenas para algunas configuraciones regionales alemanas: “ß” uppercases a “SS” ☦ pero toupper solo puede devolver una unidad de código de caracteres .

A continuación, wstring_convert / wbuffer_convert y las facetas de conversión de código estándar.

wstring_convert se usa para convertir cadenas en una encoding dada en cadenas en otra encoding dada. Hay dos tipos de cadena implicados en esta transformación, que el estándar llama una cadena de bytes y una cadena ancha. Como estos términos son realmente engañosos, prefiero usar “serializados” y “deserializados”, respectivamente, en lugar de †.

Las codificaciones a convertir entre se deciden por un codecvt (una faceta de conversión de código) pasado como argumento de tipo de plantilla a wstring_convert .

wbuffer_convert realiza una función similar pero como un búfer de secuencia deserializado amplio que envuelve un búfer de secuencia serializado de bytes . Cualquier E / S se realiza a través del búfer de secuencia serializado de bytes subyacente con conversiones hacia y desde las codificaciones dadas por el argumento codecvt. La escritura se serializa en ese búfer, y luego escribe desde allí, y la lectura se lee en el búfer y luego se deserializa.

El estándar proporciona algunas plantillas de clases de codecvt para usar con estas codecvt_utf8 : codecvt_utf8 , codecvt_utf16 , codecvt_utf8_utf16 y algunas especializaciones de codecvt . En conjunto, estas facetas estándar proporcionan todas las conversiones siguientes. (Nota: en la siguiente lista, la encoding de la izquierda es siempre la cadena serial / streambuf, y la encoding de la derecha es siempre la cadena deserializada / streambuf; el estándar permite las conversiones en ambas direcciones).

  • UTF-8 ↔ UCS-2 con codecvt_utf8 , y codecvt_utf8 donde sizeof(wchar_t) == 2 ;
  • UTF-8 ↔ UTF-32 con codecvt_utf8 , codecvt , y codecvt_utf8 donde sizeof(wchar_t) == 4 ;
  • UTF-16 ↔ UCS-2 con codecvt_utf16 , y codecvt_utf16 donde sizeof(wchar_t) == 2 ;
  • UTF-16 ↔ UTF-32 con codecvt_utf16 , y codecvt_utf16 donde sizeof(wchar_t) == 4 ;
  • UTF-8 ↔ UTF-16 con codecvt_utf8_utf16 , codecvt , y codecvt_utf8_utf16 donde sizeof(wchar_t) == 2 ;
  • estrecho ↔ ancho con codecvt
  • no-op con codecvt .

Varios de estos son útiles, pero aquí hay muchas cosas incómodas.

¡En primer lugar sustituto sagrado! ese esquema de nombres es desordenado.

Entonces, hay una gran cantidad de soporte UCS-2. UCS-2 es una encoding de Unicode 1.0 que fue reemplazada en 1996 porque solo admite el plano multilingüe básico. No sé por qué el comité consideró conveniente enfocarse en una encoding que fue reemplazada hace más de 20 años. No es que el soporte para más codificaciones sea malo ni nada, pero UCS-2 aparece muy seguido aquí.

Diría que char16_t está obviamente destinado a almacenar unidades de código UTF-16. Sin embargo, esta es una parte del estándar que piensa lo contrario. codecvt_utf8 no tiene nada que ver con UTF-16. Por ejemplo, wstring_convert>().to_bytes(u"\U0001F34C") comstackrá bien, pero fallará incondicionalmente: la entrada se tratará como la cadena UCS-2 u"\xD83C\xDF4C" , que no se puede convertir a UTF-8 porque UTF-8 no puede codificar ningún valor en el rango 0xD800-0xDFFF.

Aún en el frente UCS-2, no hay forma de leer desde un flujo de bytes UTF-16 en una cadena UTF-16 con estas facetas. Si tiene una secuencia de bytes UTF-16, no puede deserializarla en una cadena de char16_t . Esto es sorprendente, porque es más o menos una conversión de identidad. Aún más sorprendente, sin embargo, es el hecho de que hay soporte para la deserialización de un flujo UTF-16 en una cadena UCS-2 con codecvt_utf16 , que en realidad es una conversión con pérdida.

Sin embargo, el soporte UTF-16-como-bytes es bastante bueno: admite detectar endianess desde una BOM, o seleccionarlo explícitamente en el código. También admite producción con y sin BOM.

Hay algunas posibilidades de conversión más interesantes ausentes. No hay forma de deserializar de un flujo de bytes UTF-16 o cadena en una cadena UTF-8, ya que UTF-8 nunca se admite como la forma deserializada.

Y aquí el mundo estrecho / ancho está completamente separado del mundo UTF / UCS. No hay conversiones entre las codificaciones angostas / anchas de estilo antiguo y las codificaciones Unicode.

Biblioteca de entrada / salida

La biblioteca de E / S se puede usar para leer y escribir texto en codificaciones Unicode usando las wstring_convert y wbuffer_convert descritas anteriormente. No creo que haya mucho más que necesite ser respaldado por esta parte de la biblioteca estándar.

Biblioteca de expresiones regulares

Examiné problemas con expresiones regulares de C ++ y Unicode en Desbordamiento de stack antes. No repetiré todos estos puntos aquí, sino que simplemente indicaré que las expresiones regulares de C ++ no tienen soporte Unicode de nivel 1, que es el mínimo indispensable para hacer que se puedan usar sin recurrir al uso de UTF-32 en todas partes.

¿Eso es?

Si eso es. Esa es la funcionalidad existente. Hay muchas funciones de Unicode que no se ven como la normalización o los algoritmos de segmentación de texto.

U + 1F4A9 . ¿Hay alguna manera de obtener un mejor soporte de Unicode en C ++?

Los sospechosos habituales: ICU y Boost.Locale .


† Una cadena de bytes es, como era de esperar, una cadena de bytes, es decir, objetos char . Sin embargo, a diferencia de un literal de cadena ancha , que siempre es una matriz de objetos wchar_t , una “cadena ancha” en este contexto no es necesariamente una cadena de objetos wchar_t . De hecho, el estándar nunca define explícitamente lo que significa una “cuerda ancha”, por lo que nos queda adivinar el significado del uso. Dado que la terminología estándar es descuidada y confusa, utilizo la mía, en nombre de la claridad.

Las codificaciones como UTF-16 se pueden almacenar como secuencias de char16_t , que luego no tienen endianness; o pueden almacenarse como secuencias de bytes, que tienen endianness (cada par de bytes consecutivos puede representar un valor de char16_t diferente según endianness). El estándar admite ambas formas. Una secuencia de char16_t es más útil para la manipulación interna en el progtwig. Una secuencia de bytes es la forma de intercambiar tales cadenas con el mundo externo. Los términos que usaré en lugar de “byte” y “ancho” son así “serializados” y “deserializados”.

‡ Si está a punto de decir “¡pero Windows!” sostén tu 🐎🐎 . Todas las versiones de Windows desde Windows 2000 usan UTF-16.

☦ Sí, sé sobre el großes Eszett (ẞ), pero incluso si cambiaras todas las localidades alemanas durante la noche para tener ß mayúscula en ẞ, todavía hay muchos otros casos en los que esto no funcionaría. Intenta usar el uppercasing U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ. No hay ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; solo mayúsculas a dos Fs. O U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴡɪᴛʜ; no hay capital precompuesto; es solo una mayúscula a una J mayúscula y una combinación de caron.

Unicode no es compatible con la Biblioteca estándar (por ningún significado razonable de compatible).

std::string no es mejor que std::vector : es completamente ajeno a Unicode (o cualquier otra representación / encoding) y simplemente trata su contenido como una burbuja de bytes.

Si solo necesita almacenar y catenear blobs, funciona bastante bien; pero tan pronto como desee la funcionalidad Unicode (número de puntos de código, número de grafemas, …) no tendrá suerte.

La única biblioteca completa que conozco para esto es ICU. Sin embargo, la interfaz de C ++ se derivó de Java, por lo que está lejos de ser idiomática.

Puede almacenar UTF-8 de forma segura en std::string (o en un char[] o char* , para el caso), debido a que un Unicode NUL (U + 0000) es un byte nulo en UTF-8 y que esta es la única forma en que puede ocurrir un byte nulo en UTF-8. Por lo tanto, sus cadenas UTF-8 terminarán de forma adecuada de acuerdo con todas las funciones de cadenas C y C ++, y usted puede std::cerr iostreams (incluyendo std::cout y std::cerr , siempre que su configuración regional sea UTF -8).

Lo que no se puede hacer con std::string para UTF-8 es obtener longitud en los puntos de código. std::string::size() te dirá la longitud de la cadena en bytes , que solo es igual a la cantidad de puntos de código cuando estás dentro del subconjunto ASCII de UTF-8.

Si necesita operar en cadenas UTF-8 en el nivel de punto de código — no solo almacenarlas e imprimirlas — o si está tratando con UTF-16, que es probable que tenga muchos bytes nulos internos, necesita para ver los tipos de cadenas de caracteres anchos.

C ++ 11 tiene un par de nuevos tipos de cadenas literales para Unicode.

Desafortunadamente, el soporte en la biblioteca estándar para codificaciones no uniformes (como UTF-8) sigue siendo malo. Por ejemplo, no hay una forma agradable de obtener la longitud (en puntos de código) de una cadena UTF-8.

Sin embargo, hay una biblioteca bastante útil llamada tiny-utf8 , que es básicamente un reemplazo std::wstring para std::string / std::wstring . Su objective es llenar el vacío de la clase de contenedor utf8-string aún faltante.

Esta podría ser la forma más cómoda de ‘tratar’ con cadenas utf8 (es decir, sin normalización Unicode y cosas similares). Operas cómodamente en puntos de código , mientras que tu cadena permanece codificada en caracteres codificados por longitud de ejecución.