WChars, codificaciones, estándares y portabilidad

Lo siguiente puede no calificar como una pregunta de SO; si está fuera de límites, siéntase libre de decirme que me vaya. La pregunta aquí es básicamente: “¿Comprendo correctamente el estándar C y esta es la manera correcta de hacer las cosas?”

Me gustaría pedir aclaraciones, confirmaciones y correcciones sobre mi comprensión del manejo de caracteres en C (y por lo tanto C ++ y C ++ 0x). Primero, una observación importante:

Portabilidad y serialización son conceptos ortogonales.

Las cosas portátiles son cosas como C, unsigned int , wchar_t . Las cosas serializables son cosas como uint32_t o UTF-8. “Portátil” significa que puede recomstackr la misma fuente y obtener un resultado de trabajo en cada plataforma compatible, pero la representación binaria puede ser totalmente diferente (o incluso no existir, por ejemplo TCP-over-carrier paloma). Por otro lado, las cosas serializables siempre tienen la misma representación, por ejemplo, el archivo PNG que puedo leer en mi escritorio de Windows, en mi teléfono o en mi cepillo de dientes. Las cosas portátiles son cosas internas y serializables que tienen que ver con E / S. Las cosas portátiles son tipo seguras, las cosas serializables necesitan un tipo de juego de palabras.

Cuando se trata del manejo de caracteres en C, hay dos grupos de cosas relacionadas, respectivamente, con la portabilidad y la serialización:

  • wchar_t , setlocale() , mbsrtowcs() / wcsrtombs() : el estándar C no dice nada acerca de “codificaciones” ; de hecho, es completamente agnóstico a cualquier texto o propiedades de encoding. Solo dice “tu punto de entrada es main(int, char**) ; obtienes un tipo wchar_t que puede contener todos los caracteres de tu sistema; obtienes funciones para leer las secuencias de entrada y convertirlas en wstrings factibles y viceversa.

  • iconv() y UTF-8,16,32: Una función / biblioteca para transcodificar entre codificaciones definidas, definidas y fijas bien definidas. Todas las codificaciones manejadas por iconv son universalmente entendidas y aceptadas, con una excepción.

El puente entre el mundo portátil, que codifica la agnóstico de C con su tipo de carácter portátil wchar_t y el mundo determinista del exterior, es la conversión de iconv entre WCHAR-T y UTF .

Entonces, ¿debería siempre almacenar mis cadenas internamente en una interfaz wstring de encoding independiente, con el CRT a través de wcsrtombs() , y usar iconv() para la serialización? Conceptualmente:

  my program  CRT | wchar_t[] |  --- mbstowcs --> \==============/ <-- iconv(WCHAR_T, UTF8) --- | +-- iconv(WCHAR_T, UCS-4) --+ | ... <--- (adv. Unicode malarkey) ----- libicu ---+ 

Prácticamente, eso significa que escribiría dos envoltorios de placa de caldera para el punto de entrada de mi progtwig, por ejemplo, para C ++:

 // Portable wmain()-wrapper #include  #include  #include  #include  std::vector parse(int argc, char * argv[]); // use mbsrtowcs etc int wmain(const std::vector args); // user starts here #if defined(_WIN32) || defined(WIN32) #include  extern "C" int main() { setlocale(LC_CTYPE, ""); int argc; wchar_t * const * const argv = CommandLineToArgvW(GetCommandLineW(), &argc); return wmain(std::vector(argv, argv + argc)); } #else extern "C" int main(int argc, char * argv[]) { setlocale(LC_CTYPE, ""); return wmain(parse(argc, argv)); } #endif // Serialization utilities #include  typedef std::basic_string U16String; typedef std::basic_string U32String; U16String toUTF16(std::wstring s); U32String toUTF32(std::wstring s); /* ... */ 

¿Es esta la manera correcta de escribir un núcleo de progtwig idiomático, portátil, universal y que codifica la encoding usando solo C / C ++ puro y estándar, junto con una interfaz de E / S bien definida para UTF usando iconv? (Tenga en cuenta que problemas como la normalización Unicode o el reemplazo diacrítico están fuera del scope, solo después de que decida que realmente desea Unicode (a diferencia de cualquier otro sistema de encoding que desee) es hora de tratar con esos detalles, por ejemplo, usando una biblioteca dedicada como libicu.)

Actualizaciones

Después de muchos comentarios muy agradables, me gustaría agregar algunas observaciones:

  • Si su aplicación explícitamente desea tratar con texto Unicode, debe hacer que iconv -conversion parte del núcleo y use uint32_t / char32_t -strings internamente con UCS-4.

  • Windows: Si bien usar cadenas anchas generalmente es bueno, parece que la interacción con la consola (cualquier consola, para el caso) es limitada, ya que no parece ser compatible con ninguna encoding de consola de mbstowcs y mbstowcs es esencialmente inútil ( excepto para el ensanchamiento trivial). La recepción de argumentos de cadena ancha de, por ejemplo, un drop de Explorer junto con GetCommandLineW + CommandLineToArgvW funciona (tal vez debería haber un contenedor separado para Windows).

  • Sistemas de archivos: los sistemas de archivos no parecen tener ninguna noción de encoding y simplemente toman cualquier cadena terminada en nulo como un nombre de archivo. La mayoría de los sistemas toman cadenas de bytes, pero Windows / NTFS toma cadenas de 16 bits. char16_t tener cuidado al descubrir qué archivos existen y al manejar esos datos (por ejemplo, char16_t secuencias char16_t que no constituyen un UTF16 válido (por ejemplo, sustitutos desnudos) son nombres de archivo NTFS válidos). El estándar C fopen no puede abrir todos los archivos NTFS, ya que no existe una conversión posible que se correlacione con todas las cadenas posibles de 16 bits. _wfopen posible que se requiera el uso de _wfopen específico de Windows. Como corolario, en general, no existe una noción bien definida de “cuántos caracteres” componen un nombre de archivo dado, ya que no hay ninguna noción de “personaje” en primer lugar. Caveat Emptor.

¿Es esta la manera correcta de escribir un núcleo de progtwig idiomático, portátil, universal y que codifica la encoding utilizando solo C / C ++ estándar puro?

No, y no hay manera de cumplir todas estas propiedades, al menos si desea que su progtwig se ejecute en Windows. En Windows, debe ignorar los estándares C y C ++ en casi todas partes y trabajar exclusivamente con wchar_t (no necesariamente internamente, sino en todas las interfaces con el sistema). Por ejemplo, si comienzas con

 int main(int argc, char** argv) 

ya ha perdido soporte Unicode para argumentos de línea de comando. Tu tienes que escribir

 int wmain(int argc, wchar_t** argv) 

en su lugar, o use la función GetCommandLineW , ninguna de las cuales se especifica en el estándar C.

Más específicamente,

  • cualquier progtwig compatible con Unicode en Windows debe ignorar activamente el estándar de C y C ++ para cosas como argumentos de línea de comandos, E / S de archivos y consolas, o manipulación de archivos y directorios. Esto ciertamente no es idiomático . Use las extensiones o envoltorios de Microsoft como Boost.Filesystem o Qt en su lugar.
  • La portabilidad es extremadamente difícil de lograr, especialmente para el soporte de Unicode. Realmente debes estar preparado para que todo lo que piensas que sabes sea posiblemente incorrecto. Por ejemplo, debe tener en cuenta que los nombres de archivo que utiliza para abrir archivos pueden ser diferentes de los nombres de archivo que realmente se usan, y que dos nombres de archivo aparentemente diferentes pueden representar el mismo archivo. Después de crear dos archivos a y b , puede terminar con un solo archivo c , o dos archivos dy e , cuyos nombres de archivo son diferentes de los nombres de archivo que pasó al sistema operativo. O necesita una biblioteca de envoltura externa o muchos #ifdef s.
  • En general, codificar la agnosticidad no funciona en la práctica, especialmente si quieres ser portátil. Debes saber que wchar_t es una unidad de código UTF-16 en Windows y que a menudo (no siempre) una unidad de código UTF-8 en Linux. La concientización de la encoding a menudo es el objective más deseable: asegúrese de saber siempre con qué encoding trabaja, o utilice una biblioteca contenedora que los abstraiga.

Creo que tengo que concluir que es completamente imposible construir una aplicación portátil con capacidad Unicode en C o C ++ a menos que esté dispuesto a usar bibliotecas adicionales y extensiones específicas del sistema, y ​​poner mucho esfuerzo en ello. Desafortunadamente, la mayoría de las aplicaciones ya fallan en tareas comparativamente simples como “escribir caracteres griegos en la consola” o “admitir cualquier nombre de archivo permitido por el sistema de manera correcta”, y tales tareas son solo los primeros pasos hacia el verdadero soporte Unicode.

wchar_t tipo wchar_t porque depende de la plataforma (no es “serializable” según su definición): UTF-16 en Windows y UTF-32 en la mayoría de los sistemas tipo Unix. En su lugar, use los char16_t y / o char32_t de C ++ 0x / C1x. (Si no tiene un comstackdor nuevo, uint16_t como uint16_t y uint32_t por ahora).

DO define funciones para convertir entre funciones UTF-8, UTF-16 y UTF-32.

NO escriba versiones angostas / anchas sobrecargadas de cada función de cadena como lo hizo la API de Windows con -A y -W. Elija una encoding preferida para usar internamente y cúmplala. Para cosas que necesitan una encoding diferente, conviértalo según sea necesario.

El problema con wchar_t es que el procesamiento de texto wchar_t la encoding es demasiado difícil y debe evitarse. Si te quedas con “C pura” como dices, puedes usar todas las funciones w* como wcscat y amigos, pero si quieres hacer algo más sofisticado, entonces tienes que sumergirte en el abismo.

Aquí hay algunas cosas que son mucho más difíciles con wchar_t de lo que son si solo elige una de las codificaciones UTF:

  • Análisis de Javascript: los identificadores pueden contener ciertos caracteres fuera del BMP (y supongamos que te importa este tipo de corrección).

  • HTML: ¿Cómo se enciende 𐀀 en una cadena de wchar_t ?

  • Editor de texto: ¿Cómo se encuentran los límites del clúster grafema en una cadena wchar_t ?

Si conozco la encoding de una cadena, puedo examinar los caracteres directamente. Si no conozco la encoding, espero que lo que quiera hacer con una cadena sea implementado por una función de biblioteca en alguna parte. Entonces la portabilidad de wchar_t es algo irrelevante ya que no lo considero un tipo de datos especialmente útil .

Los requisitos de su progtwig pueden diferir y puede funcionar bien para usted.

Dado que iconv no es “C / C ++ estándar puro”, no creo que esté satisfaciendo sus propias especificaciones.

Hay nuevas facetas de codecvt vienen con char32_t y char16_t así que no veo cómo puedes estar equivocado, siempre y cuando seas consistente y elijas un tipo de caracteres + encoding si las facetas están aquí.

Las facetas se describen en 22.5 [locale.stdcvt] (desde n3242).


No entiendo cómo esto no satisface al menos algunos de sus requisitos:

 namespace ns { typedef char32_t char_t; using std::u32string; // or use user-defined literal #define LIT u32 // Communicate with interface0, which wants utf-8 // This type doesn't need to be public at all; I just refactored it. typedef std::wstring_convert, char_T> converter0; inline std::string to_interface0(string const& s) { return converter0().to_bytes(s); } inline string from_interface0(std::string const& s) { return converter0().from_bytes(s); } // Communitate with interface1, which wants utf-16 // Doesn't have to be public either typedef std::wstring_convert, char_T> converter1; inline std::wstring to_interface0(string const& s) { return converter1().to_bytes(s); } inline string from_interface0(std::wstring const& s) { return converter1().from_bytes(s); } } // ns 

Entonces su código puede usar ns::string , ns::char_t , LIT'A' & LIT"Hello, World!" con abandono imprudente, sin saber cuál es la representación subyacente. Luego use from_interfaceX(some_string) cuando sea necesario. Tampoco afecta la configuración regional o las secuencias. Los ayudantes pueden ser tan inteligentes como sea necesario, por ejemplo, codecvt_utf8 puede tratar con ‘encabezados’, que supongo que es estándar de cosas complicadas como la lista de materiales (ditto codecvt_utf16 ).

De hecho, escribí lo anterior para ser lo más breve posible, pero realmente querrías ayudantes como este:

 template inline ns::string ns::from_interface0(T&&... t) { return converter0().from_bytes(std::forward(t)...); } 

que le dan acceso a las 3 sobrecargas para cada [from|to]_bytes , aceptando cosas como, por ejemplo, const char* o rangos.