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, имена файлов которых отличаются от имен файлов, которые вы передали в ОС. Либо вам нужна внешняя библиотека-оболочка, либо множество
#ifdef
s. - Агностичность кодирования обычно просто не работает на практике, особенно если вы хотите быть переносимым. Вы должны знать, что
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: как вы включаете
𐀀
в строку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*
или диапазоны.