Поиск справки istreambuf_iterator <wchar_t>, чтение полного текстового файла символов Unicode

В книге "Эффективный STL" Скотта Мейерса есть хороший пример чтения целого текстового файла в объект std::string:

std::string sData; 

/*** Open the file for reading, binary mode ***/
std::ifstream ifFile ("MyFile.txt", std::ios_base::binary); // Open for input, binary mode

/*** Read in all the data from the file into one string object ***/
sData.assign (std::istreambuf_iterator <char> (ifFile),
              std::istreambuf_iterator <char> ());

Обратите внимание, что он читает его как 8-байтовые символы. Это работает очень хорошо. Недавно, хотя мне нужно прочитать файл, содержащий текст Unicode (т.е. Два байта за char). Однако, когда я пытаюсь (наивно) изменить его, чтобы читать данные из текстового файла Unicode в объект std:: wstring, например:

std::wstring wsData; 

/*** Open the file for reading, binary mode ***/
std::wifstream ifFile ("MyFile.txt", std::ios_base::binary); // Open for input, binary mode

/*** Read in all the data from the file into one string object ***/
wsData.assign (std::istreambuf_iterator <wchar_t> (ifFile),
               std::istreambuf_iterator <wchar_t> ());

Строка, которую я возвращаю, имея широкие символы, по-прежнему имеет альтернативные нули. Например, если в файле содержится строка Юникода "ABC", байты файла (игнорирование младших байтов Юникода 0xFF, 0xFE):   <A> < 0 <B> < 0 <C> < 0 >

Первый вышеописанный фрагмент кода корректно приведет к следующему содержимому строки (char):
  sData [0] = 'A
  sData [1] = 0x00
  sData [2] = 'B
  sData [3] = 0x00
  sData [4] = 'C
  sData [5] = 0x00

Однако, когда выполняется второй фрагмент кода, он нежелательно приводит к следующему содержимому строки (wchar_t):
  wsData [0] = L'A
  wsData [1] = 0x0000
  wsData [2] = L'B
  wsData [3] = 0x0000
  wsData [4] = L'C
  wsData [5] = 0x0000

Как будто файл все еще читается байтом байтом, а затем просто переводится на отдельные символы wchar_t.

Я бы подумал, что std:: istreambuf_iterator, специализирующийся на wchar_t, должен был заставить файл читать два байта за раз, не так ли? Если нет, то какова его цель?

Я проследил в шаблоны (нет легкого feat;-), и итератор действительно все еще, кажется, читает байтовый файл байтом и передает его в свою внутреннюю процедуру преобразования, которая покорно заявляет, что преобразование выполняется после каждого байт (не только после получения 2 байтов).

Я искал несколько сайтов в Интернете (включая этот) для этой кажущейся тривиальной задачи, но не нашел объяснения этого поведения или хорошей альтернативы, которая не включает в себя больше кода, чем я считаю необходимым (например, Поиск Google в Интернете производит тот же самый второй фрагмент кода, что и жизнеспособная часть кода).

Единственное, что я обнаружил, что работает, - это следующее, и я считаю, что это чит, поскольку ему нужен прямой доступ к внутреннему буферу wstrings, а затем type-coerces его при этом.

std::wstring wsData; 

/*** Open the file for reading, binary mode ***/
std::wifstream ifFile ("MyFile.txt", std::ios_base::binary); // Open for input, binary mode

wsData.resize (<Size of file in bytes> / sizeof (wchar_t));

ifFile.read ((char *) &wsData [0], <Size of file in bytes>);

О, и чтобы предотвратить неизбежное "Зачем открывать файл в двоичном режиме, почему не в текстовом режиме", этот открытый намеренно, как будто файл был открыт в текстовом режиме (по умолчанию), это означает, что CR/LF ( "\ r\n" или 0x0D0A) будут преобразованы в последовательности только LF ( "\n" или 0x0A), тогда как чистое байтовое чтение файла сохранит их. Несмотря на это, для тех, кто несгибался, изменение, которое, неудивительно, не имело никакого эффекта.

Итак, есть два вопроса: почему второй случай не работает, как можно было бы ожидать (т.е. что происходит с этими итераторами), и каков будет ваш любимый "кошерный STL-путь" для загрузки файла символов Unicode в wstring?

Что мне здесь не хватает; это должно быть что-то глупое.

Крис

Ответ 1

Вы должны связаться с SO, чтобы не получить ответы на свой первый вопрос после 4-и с половиной месяцев. Это хороший вопрос, и ответы на самые полезные вопросы (ну или плохо) в течение нескольких минут. Две вероятные причины для вашего пренебрежения:

  • Вы не отметили его как "С++", поэтому многие программисты на C++, которые могли бы помочь, никогда не будут заметил это. (Я теперь отметил его "С++".)

  • Ваш вопрос касается обработки потока unicode, что является некой идеей крутого кодирования.

Заблуждение, которое сорвало ваши исследования, похоже на это: вы, кажется, считают, что широкосимвольный поток, std::wfstream и широкоформатная строка, std::wstring, являются соответственно такими же, как "поток Unicode" и "строка Unicode", и, в частности, что они, соответственно, совпадают с потоком UTF-16 и строкой UTF-16. Ни одна из этих вещей не верна.

An std::wifstream (std::basic_ifstream<wchar_t>) - входной поток, который преобразует внешняя последовательность байтов во внутреннюю последовательность wchar_t, в соответствии с заданной или стандартное кодирование внешней последовательности.

Аналогично std::wofstream (std::basic_ofstream<wchar_t>) - выходной поток, который преобразует внутреннюю последовательность wchar_t во внешнюю последовательность байтов, согласно указанное или стандартное кодирование внешней последовательности.

И std::wstring (std::basic_string<wchar_t>) - это строковый тип, который просто хранит последовательность wchar_t, без знания кодировки - if-any, из которой они привели.

Unicode - это семейство кодировок с байтовой последовательностью - UTF-8/-16/-32 и еще несколько неясных других - связанный по принципу, что UTF-N кодирует алфавиты, используя последовательность из 1 или более N-битовые единицы на символ. UTF-16, по-видимому, является кодировкой, которую вы пытаетесь прочитать в std::wstring. Вы говорите:

Я бы подумал, что std:: istreambuf_iterator, специализирующийся на wchar_t, должен был привести к тому, что файл читался два байта за раз, не так ли? Если нет, то какова его цель?

Но как только вы узнаете, что wchar_t не обязательно 2 байта (это в библиотеках Microsoft C, как 32, так и 64-битные, но в GCC - 4 байта), а также что кодовая точка UTF-16 (символ) не нужно вписываться в 2 байта (для этого может потребоваться 4), вы увидите, что указание выделения единица wchar_t не может быть все, что нужно для декодирования потока UTF-16.

Когда вы создаете и открываете свой входной поток:

std::wifstream ifFile ("MyFile.txt", std::ios_base::binary);

Готово извлечь символы (из некоторого алфавита) из "MyFile.txt" в значения типа wchar_t, и он будет извлекать эти символы из байтовой последовательности в файл в соответствии с кодировкой, указанной std::locale который работает в потоке, когда он выполняет извлечение.

В вашем коде не указывается std::locale для вашего потока, поэтому значение по умолчанию вступает в силу. Это значение по умолчанию - это глобальная локаль С++, которая по умолчанию является "C" локаль; и язык "C" предполагает "кодирование идентичности" последовательностей байтов ввода-вывода, то есть 1 байт = 1 символ ( исключая исключение новой строки для ввода/вывода текстового режима).

Таким образом, когда вы используете std::istreambuf_iterator<wchar_t> для извлечение символов, извлечение продолжается путем преобразования каждого байта в файле wchar_t, который он добавляет к std::wstring wsData. Байты в файле, как вы говорите:

0xFF, 0xFE, 'A', 0x00, 'B', 0x00, 'C', 0x00

Первые два, которые вы опускаете как "младшие байты Unicode", действительно являются UTF-16 (BOM), но в кодировке по умолчанию они просто являются тем, чем они являются.

Соответственно, широкие символы, присвоенные wsData, как вы заметили:

0x00FF, 0x00FE, L'A ', 0x0000, L'B', 0x0000, L'C ', 0x0000

Это как если бы файл все еще читался байтом байтом, а затем просто переводился в отдельные символы wchar_t.

потому что это именно то, что происходит.

Чтобы остановить это, вам нужно что-то сделать, прежде чем вы начнете извлекать символы из потока чтобы сказать, что он должен декодировать последовательность символов UTF-16. Способ сделать это концептуально довольно извилистая. Вам нужно imbue поток с std::locale, который обладает std::locale::facet, который является экземпляром std::codecvt<InternT, ExternT, StateT>(или получен из таких) который предоставит потоку правильные методы из декодирования UTF-16 в wchar_t.

Но суть в том, что вам нужно подключить нужный кодировщик/декодер UTF-16 в поток и на практике это (или должно быть) достаточно просто. Я предполагаю, что ваш компилятор - это недавний MS VС++. Если это право, то вы можете исправить свой код:

  • Добавление #include <locale> и #include <codecvt> в заголовки
  • Добавление строки:

    ifFile.imbue(std::locale(ifFile.getloc(),new std::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>));

сразу после:

std::wifstream ifFile ("MyFile.txt", std::ios_base::binary);

Эффект этой новой строки заключается в том, чтобы "imbue" ifFile создать новую локаль, которая является той же как тот, который у него уже был - ifFile.getloc() - но с измененной фазой кодировщика/декодера  - std::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>. Этот фасет codecvt который будет декодировать символы UTF-16 с максимальным значением 0x10ffff в little-endian wchar_t значения (0x10ffff являются максимальным значением кодовых точек UTF-16).

Когда вы отлаживаетесь в исправленный таким образом код, вы теперь обнаружите, что wsData имеет только 4 широкоформатных символа и что эти символы:

0xFEFF, L'A', L'B', L'C'

как вы ожидаете от них, причем первый из них представляет собой спецификацию UTF-16 little-endian.

Обратите внимание, что порядок FE, FF является обратным тому, что было до применения факела codecvt, показывая нам, что декодирование с малым порядком было выполнено по запросу. И это должно было быть. Просто отредактируйте новую строку, удалив std::little_endian, снова отлаживаем его, и вы обнаружите, что первый элемент wsData становится 0xFFFE и что другие три широких символа становятся пиктограммами IICore пиктографический набор символов (если ваш отладчик может отображать их). (Теперь, когда коллега жалуется, что их код превращает английский Unicode в "китайский", вы узнаете вероятное объяснение.)

Если вы хотите заполнить wsData без ведущей спецификации, вы можете сделать это снова изменить новую строку и заменить std::little_endian на std::codecvt_mode(std::little_endian|std::consume_header)

Наконец, вы, возможно, заметили ошибку в новом коде, а именно, что 2 байта wchar_t недостаточно широко для представления кодовых точек UTF-16 между 0x100000 и 0x10ffff который можно прочитать.

Вам это не удастся, пока все кодовые коды, которые вы должны прочитать, лежат в UTF-16 Основной многоязычный план, который охватывает [0,0xffff], и вы можете знать, что все входы будут вечно подчиняться этому ограничение. В противном случае 16-разрядный wchar_t не подходит для цели. Заменить:

  • wchar_t с помощью char32_t
  • std::wstring с std::basic_string<char32_t>
  • std::wifstream с std::basic_ifstream<char32_t>

и код полностью подходит для чтения произвольного кодированного UTF-16 файла в строку.

(Читатели, работающие с библиотекой GNU С++, найдут, что с v4.7.2 он еще не предоставляет стандартный заголовок <codecvt>. Заголовок <bits/codecvt.h> существует и, предположительно, когда-нибудь будет выпускником, чтобы быть <codecvt>, но на данный момент он только экспортирует специализации class codecvt<char, char, mbstate_t> и class codecvt<wchar_t, char, mbstate_t>, которые являются соответственно тождеством преобразования и преобразования между ASCII/UTF-8 и wchar_t. Чтобы решить проблему ОП вам потребуется подкласс std::codecvt<wchar_t,char,std::char_traits<wchar_t>::state_type> себя, как этот ответ)