WChars, Кодировки, Стандарты и Переносимость - PullRequest
60 голосов
/ 10 июня 2011

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

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

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

Переносные вещи - это такие вещи, как C, 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 переносимым символьным типом и детерминированным внешним миром - это преобразование iconv между 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 по существу бесполезен (кроме как для тривиального расширения). Получение аргументов с широкими строками, скажем, из проводника Explorer вместе с GetCommandLineW + CommandLineToArgvW работает (возможно, для Windows должна быть отдельная оболочка).

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

Ответы [ 4 ]

21 голосов
/ 12 июня 2011

Это правильный способ написания идиоматического, портативного, универсального, независимого от кодирования программного ядра, использующего только чистый стандарт 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.

9 голосов
/ 10 июня 2011

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

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

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

8 голосов
/ 11 июня 2011

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

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

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

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

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

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

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

6 голосов
/ 10 июня 2011

Учитывая, что 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* или диапазоны.

...