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

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

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

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

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

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

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

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

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

Таким образом, я должен всегда хранить свои строки внутри независимой от кодирования 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 ---+

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

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

Обновления

После многих очень хороших комментариев я хотел бы добавить несколько замечаний:

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

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

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

4 ответа

Решение

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

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

int main(int argc, char** argv)

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

int wmain(int argc, wchar_t** argv)

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

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

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

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

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

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

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

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

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

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

  • HTML: как вы включаете &#65536; в строку wchar_t?

  • Текстовый редактор: Как найти границы кластера графемы в wchar_t строка?

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

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

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

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

Фасеты описаны в 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 может иметь дело с "заголовками", которые, как я полагаю, являются стандартными из таких хитрых вещей, как спецификация (то же самое 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* или диапазоны.

Другие вопросы по тегам