¿Qué tan bien es compatible Unicode en C ++ 11?

183

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

  • ¿Qué tan bien admite la biblioteca estándar de C ++ Unicode?
  • ¿Hace std::stringlo que debería?
  • ¿Como lo uso?
  • ¿Dónde están los posibles problemas?
Ralph Tandetzky
fuente
19
"¿Std :: string hace lo que debería?" ¿Qué crees que debería hacer?
R. Martinho Fernandes
2
Uso utfcpp.sourceforge.net para mis necesidades de utf8. Es un archivo de encabezado simple que proporciona iteradores para cadenas unicode.
fscan
2
std :: string debe almacenar bytes, es decir, la secuencia de la unidad de código de la codificación UTF-8. Sí, hace exactamente eso, desde el principio. utf8everywhere.org
Pavel Radzivilovsky
3
Los mayores problemas potenciales con el soporte Unicode se encuentran dentro de Unicode y su uso en la tecnología de la información. Unicode no es adecuado (y no está diseñado) para lo que se usa. Unicode está diseñado para reproducir cada glifo posible que haya sido escrito en alguna parte por alguien, en algún momento con cada matiz improbable y pedante posible, incluyendo 3 o 4 significados diferentes y 3 o 4 formas diferentes de componer el mismo glifo. No está destinado a ser útil para ser utilizado en el lenguaje cotidiano, y no está destinado a ser aplicable ni a ser procesado fácil o inequívocamente.
Damon
11
Sí, está diseñado para usarse en el lenguaje cotidiano. La mía al menos. Y el tuyo muy probablemente también. Simplemente resulta que procesar texto humano de manera general es una tarea muy difícil. Ni siquiera es posible definir inequívocamente qué es un personaje. La reproducción general de glifos ni siquiera es realmente parte de la carta Unicode.
Jean-Denis Muys

Respuestas:

267

¿Qué tan bien admite la biblioteca estándar de C ++ Unicode?

Terriblemente.

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

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

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

¿Hace std::stringlo que debería?

Si. De acuerdo con el estándar C ++, esto es lo que std::stringdeberían hacer y sus hermanos:

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

Bueno, std::stringeso está bien. ¿Proporciona eso alguna funcionalidad específica de Unicode? No.

¿Deberia? Probablemente no. std::stringestá bien como una secuencia de charobjetos. Eso es útil; La única molestia es que es una vista de texto de muy bajo nivel y C ++ estándar no proporciona una vista de nivel superior.

¿Como lo uso?

Úselo como una secuencia de charobjetos; pretender que es algo más está destinado a terminar en dolor.

¿Dónde están los posibles problemas?

¿Por todo el lugar? Veamos...

Biblioteca de cadenas

La biblioteca de cadenas nos proporciona basic_string, que es simplemente una secuencia de lo que el estándar llama "objetos 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 estrecho y el mundo Unicode: c16rtomb/ mbrtoc16y c32rtomb/ mbrtoc32.

Biblioteca de localización

La biblioteca de localización todavía cree que uno de esos "objetos tipo char" equivale a un "personaje". Por supuesto, esto es una tontería y hace que sea imposible hacer que muchas cosas funcionen 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 <locale>encabezado:

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

¿Cómo espera que alguna de estas funciones clasifique adecuadamente, por ejemplo, U + 1F34C as, como en u8"🍌"o u8"\U0001F34C"? No hay forma de que funcione, porque esas funciones toman solo una unidad de código como entrada.

Esto podría funcionar con un entorno local apropiado si char32_tsolo usara : U'\U0001F34C'es una unidad de código único en UTF-32.

Sin embargo, eso todavía significa que solo obtienes las transformaciones simples de mayúsculas touppery tolower, que, por ejemplo, no son lo suficientemente buenas para algunas configuraciones regionales alemanas: mayúsculas "ß" a "SS" ☦ pero touppersolo puede devolver una unidad de código de caracteres .

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

wstring_convertse utiliza para convertir entre cadenas en una codificación dada en cadenas en otra codificación dada. Hay dos tipos de cadena involucrados en esta transformación, que el estándar llama una cadena de bytes y una cadena ancha. Dado que estos términos son realmente engañosos, prefiero usar "serializado" y "deserializado", respectivamente, en su lugar †.

Las codificaciones para convertir se deciden mediante un codecvt (una faceta de conversión de código) que se pasa como un argumento de tipo plantilla a wstring_convert.

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

La norma proporciona algunas plantillas de clase codecvt para su uso con estas instalaciones: codecvt_utf8, codecvt_utf16, codecvt_utf8_utf16, y algunas codecvtespecializaciones. Juntas, estas facetas estándar proporcionan las siguientes conversiones. (Nota: en la siguiente lista, la codificación de la izquierda es siempre la cadena / streambuf serializada, y la codificación de la derecha siempre es la cadena / streambuf deserializada; el estándar permite conversiones en ambas direcciones).

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

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

En primer lugar, ¡santo sustituto alto! ese esquema de nombres es desordenado.

Entonces, hay mucho soporte para UCS-2. UCS-2 es una codificación 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 centrarse en una codificación que fue reemplazada hace más de 20 años. No es que el soporte para más codificaciones sea malo o algo así, pero UCS-2 aparece con demasiada frecuencia aquí.

Yo diría que char16_tobviamente está destinado a almacenar unidades de código UTF-16. Sin embargo, esta es una parte del estándar que piensa lo contrario. codecvt_utf8<char16_t>no tiene nada que ver con UTF-16. Por ejemplo, wstring_convert<codecvt_utf8<char16_t>>().to_bytes(u"\U0001F34C")compilará 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 una secuencia de bytes UTF-16 en una cadena UTF-16 con estas facetas. Si tiene una secuencia de UTF-16 bytes, no puede deserializarla en una cadena de char16_t. Esto es sorprendente, porque es más o menos una conversión de identidad. Sin embargo, aún más sorprendente es el hecho de que existe soporte para la deserialización de una secuencia UTF-16 en una cadena UCS-2 con codecvt_utf16<char16_t>, que en realidad es una conversión con pérdida.

Sin embargo, el soporte UTF-16-como-bytes es bastante bueno: admite la detección de endianess desde una lista de materiales, o seleccionarla explícitamente en el código. También es compatible con la producción de salida con y sin una lista de materiales.

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

Y aquí el mundo estrecho / ancho está completamente separado del mundo UTF / UCS. No hay conversiones entre las codificaciones estrechas / 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 utilizando las funciones wstring_converty wbuffer_convertdescritas anteriormente. No creo que haya mucho más que deba ser compatible con esta parte de la biblioteca estándar.

Biblioteca de expresiones regulares

He expuesto problemas con expresiones regulares de C ++ y Unicode en Stack Overflow antes. No repetiré todos esos puntos aquí, sino que simplemente declararé que las expresiones regulares C ++ no tienen soporte Unicode de nivel 1, que es el mínimo para hacerlas utilizables sin recurrir al uso de UTF-32 en todas partes.

¿Eso es?

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

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

Los sospechosos habituales: UCI y Boost.Locale .


† Una cadena de bytes es, como era de esperar, una cadena de bytes, es decir, charobjetos. Sin embargo, a diferencia de un literal de cadena ancha , que siempre es una matriz de wchar_tobjetos, una "cadena ancha" en este contexto no es necesariamente una cadena de wchar_tobjetos. De hecho, el estándar nunca define explícitamente lo que significa una "cadena ancha", por lo que nos queda adivinar el significado del uso. Como la terminología estándar es descuidada y confusa, uso 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 consecutivo de bytes puede representar un char16_tvalor diferente dependiendo de la endianness). El estándar admite ambas formas. Una secuencia de char16_tes más útil para la manipulación interna en el programa. 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 "wide" se "serializan" y "deserializan".

‡ 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 cambiara todos los locales alemanes de la noche a la mañana para tener ß en mayúscula a ẞ, todavía hay muchos otros casos en los que esto podría fallar. Prueba con mayúsculas U + FB00 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟɪɢᴀᴛᴜʀᴇ ғғ. No hay ʟᴀᴛɪɴ ᴄᴀᴘɪᴛᴀʟ ʟɪɢᴀᴛᴜʀᴇ ғғ; solo se pone en mayúsculas a dos Fs. O U + 01F0 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴊ ᴡɪᴛʜ ᴄᴀʀᴏɴ; no hay capital precompuesto; solo se escribe en mayúscula a una J mayúscula y un caron combinado.

R. Martinho Fernandes
fuente
26
Cuanto más lo leía, más me daba la sensación de no entender nada de todo esto. Leí la mayoría de estas cosas hace un par de meses y todavía siento que estoy descubriendo todo de nuevo ... Para que sea simple para mi pobre cerebro que ahora duele un poco, todos estos consejos en utf8everywhere todavía son válidos, ¿Derecha? Si "solo" quiero que mis usuarios puedan abrir y escribir archivos sin importar la configuración de su sistema, puedo preguntarles el nombre del archivo, almacenarlo en una cadena std :: y todo debería funcionar correctamente, incluso en Windows. Lamento preguntar eso (otra vez) ...
Uflex
55
@Uflex Todo lo que realmente puedes hacer con std :: string es tratarlo como un blob binario. En una implementación adecuada de Unicode, ni lo interno (porque está oculto en los detalles de la implementación) ni lo externo (no es necesario tener codificador / decodificador disponible).
Cat Plus Plus
3
@Uflex tal vez. No sé si seguir un consejo que no entiendes es una buena idea.
R. Martinho Fernandes
1
Hay una propuesta de soporte Unicode en C ++ 2014/17. Sin embargo, eso es 1, quizás a 4 años de distancia y de poca utilidad ahora. open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3572.html
graham.reeds
20
@ graham.reeds jaja, gracias, pero estaba al tanto de eso. Consulte la sección "Agradecimientos";)
R. Martinho Fernandes
40

Unicode no es compatible con la Biblioteca estándar (para cualquier significado razonable de compatible).

std::stringno es mejor que std::vector<char>: es completamente ajeno a Unicode (o cualquier otra representación / codificación) y simplemente trata su contenido como una gota de bytes.

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

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

Matthieu M.
fuente
2
¿Qué tal Boost.Locale ?
Uflex
11
@Uflex: desde la página que ha vinculado Para lograr este objetivo, Boost.Locale utiliza la biblioteca de localización y Unicode de última generación: UCI - Componentes internacionales para Unicode.
Matthieu M.
1
Boost.Locale es compatible con otros backends que no son de UCI, vea aquí: boost.org/doc/libs/1_53_0/libs/locale/doc/html/…
Superfly Jon
@SuperflyJon: Cierto, pero de acuerdo con esa misma página, el soporte para Unicode de los backends que no son de UCI está "severamente limitado".
Matthieu M.
24

Puede almacenar UTF-8 de forma segura en un std::string(o en un char[]o char*, para el caso), debido al hecho de que un NUL de Unicode (U + 0000) es un byte nulo en UTF-8 y que esta es la única forma de un nulo el byte puede ocurrir en UTF-8. Por lo tanto, sus cadenas UTF-8 se terminarán correctamente de acuerdo con todas las funciones de cadena C y C ++, y puede distribuirlas con iostreams C ++ (incluidos std::couty std::cerr, siempre que su ubicación sea UTF-8).

Lo que no puede hacer con std::stringUTF-8 es obtener longitud en puntos de código. std::string::size()le indicará la longitud de la cadena en bytes , que solo es igual al número de puntos de código cuando se encuentra dentro del subconjunto ASCII de UTF-8.

Si necesita operar en cadenas UTF-8 en el nivel del punto de código (es decir, no solo almacenarlas e imprimirlas) o si está tratando con UTF-16, que probablemente tenga muchos bytes nulos internos, debe investigar los tipos de cadena de caracteres anchos.

uckelman
fuente
3
std::stringpuede lanzarse a iostreams con nulos incrustados muy bien.
R. Martinho Fernandes
3
Está totalmente destinado. No se rompe c_str()en absoluto porque size()aún funciona. Solo se rompen las API rotas (es decir, aquellas que no pueden manejar nulos incrustados como la mayoría del mundo C).
R. Martinho Fernandes
1
Los nulos incrustados se rompen c_str()porque c_str()se supone que devuelve los datos como una cadena C terminada en nulo, lo cual es imposible, debido al hecho de que las cadenas C no pueden tener nulos incrustados.
uckelman
44
Ya no. c_str()ahora simplemente devuelve lo mismo data(), es decir, todo. Las API que toman un tamaño pueden consumirlo. API que no, no pueden.
R. Martinho Fernandes
66
Con la ligera diferencia que c_str()asegura que el resultado sea seguido por un objeto tipo NUL char, y no creo que lo data()haga. No, parece que data()ahora también lo hace. (Por supuesto, esto no es necesario para las API que consumen el tamaño en lugar de inferirlo de una búsqueda de terminador)
Ben Voigt
8

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 buena manera de obtener la longitud (en puntos de código) de una cadena UTF-8.

Algún tipo programador
fuente
Entonces, ¿aún necesitamos usar std :: wstring para los nombres de archivos si queremos admitir idiomas no latinos? Porque los nuevos literales de cadena realmente no ayudan aquí ya que la cadena generalmente proviene del usuario ...
Uflex
77
@Uflex std::stringpuede contener una cadena UTF-8 sin problemas, pero, por ejemplo, el lengthmétodo devuelve el número de bytes en la cadena y no el número de puntos de código.
Algún tipo programador el
8
Para ser sincero, obtener la longitud en puntos de código de una cadena no tiene muchos usos. La longitud en bytes se puede usar para preasignar búferes correctamente, por ejemplo.
R. Martinho Fernandes
2
El número de puntos de código en una cadena UTF-8 no es un número muy interesante: se puede escribir ñcomo 'LETRA PEQUEÑA LATINA N CON TILDE' (U + 00F1) (que es un punto de código) o 'LETRA PEQUEÑA LATINA N' ( U + 006E) seguido de 'TILDE COMBINADO' (U + 0303) que son dos puntos de código.
Martin Bonner apoya a Monica el
Todos esos comentarios sobre "no necesitas esto y no necesitas" como "número de puntos de código sin importancia", etc., me parecen un poco sospechosos. Una vez que escribe un analizador que debe analizar el código fuente utf8, depende de la especificación del analizador si considera o no LATIN SMALL LETTER N' == (U+006E) followed by 'COMBINING TILDE' (U+0303).
BitTickler
4

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

Esta podría ser la forma más cómoda de "lidiar" con cadenas utf8 (es decir, sin normalización Unicode y cosas similares). Puede operar cómodamente en puntos de código , mientras que su cadena permanece codificada en chars codificados por longitud de ejecución .

Jakob Riedle
fuente