WChars, кодировки, стандарты и переносимость

Следующие не могут квалифицироваться как вопрос SO; если это вне пределов, пожалуйста, не стесняйтесь сказать мне уйти. Вопрос здесь в основном: "Правильно ли я понимаю стандарт C и правильно ли это происходит?"

Я хотел бы попросить разъяснения, подтверждения и исправления в моем понимании обработки символов в C (и, следовательно, С++ и С++ 0x). Во-первых, важное замечание:

Переносимость и сериализация - это ортогональные понятия.

Переносимые вещи - это такие вещи, как C, unsigned int, wchar_t. Сериализуемые вещи - это такие вещи, как uint32_t или UTF-8. "Portable" означает, что вы можете перекомпилировать один и тот же источник и получить рабочий результат на каждой поддерживаемой платформе, но двоичное представление может быть совершенно другим (или даже не существует, например, TCP-over-carrier pigeon). Сериализуемые вещи, с другой стороны, всегда имеют одинаковое представление, например. файл PNG, который я могу прочитать на рабочем столе Windows, на моем телефоне или на моей зубной щетке. Переносимые вещи - это внутренние, сериализуемые вещи, связанные с I/O. Переносимые вещи являются типичными, сериализуемыми вещами, требующими типа punning. </преамбула >

Когда дело доходит до обработки символов в C, есть две группы вещей, связанные соответственно с переносимостью и сериализацией:

  • wchar_t, setlocale(), mbsrtowcs()/wcsrtombs(): Стандарт C ничего не говорит о "кодировках" ; на самом деле, он полностью агностик для любых свойств текста или кодирования. Он только говорит: "Ваша точка входа main(int, char**), вы получаете тип wchar_t, который может содержать все ваши системные символы, вы получаете функции для считывания входных char -последовательностей и превращения их в работоспособные wstrings и наоборот.

  • iconv() и UTF-8,16,32: функция/библиотека для перекодирования между четко определенными, определенными фиксированными кодировками. Все кодировки, обрабатываемые iconv, повсеместно понятны и согласованы, за одним исключением.

Мост между переносимым, кодирующим-агностическим миром C с его переносным символьным типом wchar_t и детерминированным внешним миром - это преобразование iconv между WCHAR-T и UTF.

Итак, должен ли я всегда хранить свои строки внутри кодировки-agnostic wstring, взаимодействовать с CRT через wcsrtombs() и использовать iconv() для сериализации? Концептуально:

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

Практически это означает, что я бы написал две обертки для котельной пластины для моей точки входа в программу, например. для С++:

// Portable wmain()-wrapper
#include <clocale>
#include <cwchar>
#include <string>
#include <vector>

std::vector<std::wstring> parse(int argc, char * argv[]); // use mbsrtowcs etc

int wmain(const std::vector<std::wstring> args); // user starts here

#if defined(_WIN32) || defined(WIN32)
#include <windows.h>
extern "C" int main()
{
  setlocale(LC_CTYPE, "");
  int argc;
  wchar_t * const * const argv = CommandLineToArgvW(GetCommandLineW(), &argc);
  return wmain(std::vector<std::wstring>(argv, argv + argc));
}
#else
extern "C" int main(int argc, char * argv[])
{
  setlocale(LC_CTYPE, "");
  return wmain(parse(argc, argv));
}
#endif
// Serialization utilities

#include <iconv.h>

typedef std::basic_string<uint16_t> U16String;
typedef std::basic_string<uint32_t> U32String;

U16String toUTF16(std::wstring s);
U32String toUTF32(std::wstring s);

/* ... */

Является ли это правильным способом создания идиоматического, переносимого, универсального ядра кодирования-агностики, использующего только чистый стандартный C/С++, а также хорошо определенный интерфейс ввода-вывода для UTF с помощью iconv? (Обратите внимание, что такие проблемы, как нормализация Unicode или диакритическая замена, находятся за пределами области действия, и только после того, как вы решите, что на самом деле хотите использовать Unicode (в отличие от любой другой системы кодирования, которую вы можете представить), пора использовать эти особенности, например, используя специальную библиотеку как libicu.)

Обновление

Следуя многим очень приятным комментариям, я хотел бы добавить несколько замечаний:

  • Если ваше приложение явно хочет иметь дело с текстом Unicode, вы должны сделать iconv -конверсионную часть ядра и использовать uint32_t/char32_t -строки внутри с UCS-4.

  • Windows: при использовании широких строк, как правило, отлично, кажется, что взаимодействие с консолью (любая консоль, если на то пошло) ограничено, так как, похоже, не поддерживается какая-либо разумная многобайтовая консольная кодировка и mbstowcs по существу бесполезен (кроме тривиального расширения). Получение широкоформатных аргументов из, скажем, проводника-обозревателя вместе с GetCommandLineW + CommandLineToArgvW работает (возможно, для Windows должна быть отдельная оболочка).

  • Файловые системы: Файловые системы, похоже, не имеют понятия кодирования и просто берут любую строку с нулевым завершением в качестве имени файла. Большинство систем берут байтовые строки, но Windows/NTFS принимает 16-битные строки. Вы должны следить за тем, какие файлы существуют и когда обрабатывать эти данные (например, char16_t последовательности, которые не являются действительными UTF16 (например, голые суррогаты), являются действительными именами файлов NTFS). Стандарт C fopen не может открыть все файлы NTFS, поскольку нет возможного преобразования, которое будет отображаться для всех возможных 16-разрядных строк. Может потребоваться использование _wfopen для Windows. В качестве следствия, как правило, нет четкого представления о том, "сколько символов" содержит заданное имя файла, поскольку вначале нет понятия "символ". Предостережение emptor.

Ответ 1

Является ли это правильным способом создания идиоматического, переносимого, универсального ядра кодирования-агностики, использующего только чистый стандартный C/С++

Нет, и вообще не нужно выполнять все эти свойства, по крайней мере, если вы хотите, чтобы ваша программа запускалась в Windows. В Windows вы должны игнорировать стандарты C и С++ почти везде и работать исключительно с wchar_t (не обязательно внутренне, но на всех интерфейсах к системе). Например, если вы начинаете с

int main(int argc, char** argv)

вы уже потеряли поддержку Unicode для аргументов командной строки. Вы должны написать

int wmain(int argc, wchar_t** argv)

или используйте функцию GetCommandLineW, ни одна из которых не указана в стандарте C.

Более конкретно,

  • любая совместимая с Unicode программа в Windows должна активно игнорировать стандарт C и С++ для таких вещей, как аргументы командной строки, файлы и консольные операции ввода-вывода или файлы и каталоги. Это, конечно, не идиоматично. Используйте расширения Microsoft или обертки, такие как Boost.Filesystem или Qt.
  • Переносимость чрезвычайно трудно достичь, особенно для поддержки Unicode. Вы действительно должны быть готовы к тому, что все, что, по вашему мнению, вы знаете, возможно, неверно. Например, вы должны учитывать, что имена файлов, которые вы используете для открытия файлов, могут отличаться от имен файлов, которые фактически используются, и что два, казалось бы, разных имени файла могут представлять один и тот же файл. После создания двух файлов a и b вы можете получить один файл c или два файла d и e, имена файлов которых отличаются от имен файлов, которые вы передали ОС. Либо вам нужна внешняя библиотека обертки, либо много #ifdef s.
  • Агностичность кодирования обычно просто не работает на практике, особенно если вы хотите быть переносимой. Вы должны знать, что wchar_t - это кодовый блок UTF-16 в Windows и что char часто (не всегда) - это код UTF-8 в Linux. Кодирование-осведомленность часто является более желательной целью: убедитесь, что вы всегда знаете, с какой кодировкой вы работаете, или используйте библиотеку-оболочку, которая абстрагирует их.

Я думаю, что я должен заключить, что полностью невозможно создать переносимое приложение, совместимое с Unicode, на C или С++, если вы не захотите использовать дополнительные библиотеки и расширения для системы и приложить много усилий. К сожалению, большинство приложений уже терпят неудачу при сравнительно простых задачах, таких как "написание греческих символов на консоль" или "поддержка любого имени файла, разрешенного системой в правильном порядке", и такие задачи - это только первые крошечные шаги в направлении поддержки истинного Unicode.

Ответ 2

Я бы избегал типа wchar_t, потому что он зависел от платформы (не "сериализуемый" по вашему определению): UTF-16 для Windows и UTF-32 для большинства Unix-подобных систем. Вместо этого используйте типы char16_t и/или char32_t из С++ 0x/C1x. (Если у вас нет нового компилятора, напечатайте их как uint16_t и uint32_t).

DO определяют функции для преобразования между функциями UTF-8, UTF-16 и UTF-32.

НЕ писать писать перегруженные узкие/широкие версии каждой строковой функции, например, API Windows с -A и -W. Выберите одну предпочтительную кодировку для использования внутри себя и придерживайтесь ее. Для вещей, которые нуждаются в другой кодировке, конвертируйте при необходимости.

Ответ 3

Проблема с wchar_t заключается в том, что обработка текстового текста с кодировкой слишком сложна и ее следует избегать. Если вы придерживаетесь "чистого C", как вы говорите, вы можете использовать все функции w*, такие как wcscat и друзей, но если вы хотите сделать что-то более сложное, вам нужно погрузиться в бездну.

Вот некоторые вещи, которые намного сложнее с wchar_t, чем они есть, если вы просто выбираете один из кодировок UTF:

  • Анализ Javascript: Идентификаторы могут содержать определенные символы вне BMP (и позволяют предположить, что вы заботитесь об этой правильности).

  • HTML: Как вы превращаете &#65536; в строку wchar_t?

  • Текстовый редактор: как вы находите граничные границы grapheme в строке wchar_t?

Если я знаю кодировку строки, я могу проверить символы непосредственно. Если я не знаю кодировку, я должен надеяться, что все, что я хочу делать со строкой, реализуется с помощью библиотечной функции где-то. Поэтому переносимость wchar_t несколько неактуальна, так как я не считаю это особенно полезным типом данных.

Ваши требования к программе могут отличаться, и wchar_t может работать нормально для вас.

Ответ 4

Учитывая, что iconv не является "чистым стандартом C/С++", я не думаю, что вы удовлетворяете свои собственные спецификации.

Появились новые грани codecvt с char32_t и char16_t, поэтому я не вижу, как вы можете ошибаться, если вы согласны, и выберите один тип char типа +, если грани здесь.

Границы описаны в 22.5 [locale.stdcvt] (из n3242).


Я не понимаю, как это не удовлетворяет хотя бы некоторым из ваших требований:

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<std::codecvt_utf8<char_T>, 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<std::codecvt_utf16<char_T>, 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

Тогда ваш код может использовать ns::string, ns::char_t, LIT'A' и LIT"Hello, World!" безрассудным отказом, не зная, что такое базовое представление. Затем используйте from_interfaceX(some_string) всякий раз, когда это необходимо. Это не влияет на глобальную локаль или потоки. Помощники могут быть настолько умными, насколько это необходимо, например. codecvt_utf8 может иметь дело с "заголовками", которые, как я полагаю, являются стандартными из сложного материала, такого как спецификация (ditto codecvt_utf16).

На самом деле я написал выше, чтобы быть как можно короче, но вы действительно хотели бы, чтобы такие помощники:

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

которые дают вам доступ к 3 перегрузкам для каждого члена [from|to]_bytes, принимая такие вещи, как, например, const char* или диапазоны.