СЕНТЯБРЬ 2016

ТОМ 31, НОМЕР 9

C++ - Преобразования Unicode-кодировок с помощью STL-строк и Win32 API

Джованни Диканио | СЕНТЯБРЬ 2016

Исходный код можно скачать по ссылке

Продукты и технологии:

C++ Unicode, Win32 API, STL-строки

В статье рассматриваются:

  • кодировки UTF-8 и UTF-16;
  • преобразование между UTF-8 и UTF-16 через Win32 API;
  • обертывание MultiByteToWideChar в C++-функцию в современном стиле для преобразований UTF-8 в UTF-16;
  • обработка ошибок с помощью C++-исключений.

Unicode является стандартом де-факто для представления текста на любых языках в современном программном обеспечении. Согласно веб-сайту официального консорциума Unicode (bit.ly/1Rtdulx), “Unicode предоставляет уникальное число для каждого символа независимо от платформы, программы и языка.” Каждое из этих уникальных чисел называют кодовой точкой (code point), и обычно она представляется с использованием префикса “U+”, за которым следует уникальное число в шестнадцатеричной форме. Так, кодовая точка, связанная с символом “C” — U+0043. Заметьте, что Unicode является отраслевым стандартом, охватывающим большинство систем письма в мире, включая иероглифы. Например, японская кана (ru.wikipedia.org/wiki/%D0%9A%D0%B0%D0%BD%D0%B0) содержит иероглиф 学 (среди других значений обозначающий обучение и знание), который связан с кодовой точкой U+5B66. В настоящее время в стандарте Unicode определены более 1 114 000 кодовых точек.

От абстрактных кодовых точек к реальным битам: кодировки UTF-8 и UTF-16

Кодовая точка — это абстрактная концепция. Для программиста вопрос заключается в том, как эти кодовые точки Unicode представляются в конкретном виде, используя биты? Ответ на этот вопрос ведет прямо к концепции Unicode-кодировки. По сути, Unicode-кодировка — конкретный, четко определенный способ представления значений кодовых точек Unicode в битах. В стандарте Unicode определено несколько кодировок, но наиболее важные из них — UTF-8 и UTF-16; обе они являются кодировками переменной длины (variable-length encodings), способными кодировать все возможные символы Unicode или, точнее, кодовые точки. Поэтому преобразования между этими двумя кодировками осуществляются без потерь: ни один символ Unicode не будет потерян в ходе этого процесса.

В UTF-8, как и предполагает название этой кодировки, используются восьмибитовые кодовые единицы (code units). Она была разработана с учетом двух важных характеристик. Во-первых, она обратно совместима с ASCII; это означает, что каждый допустимый ASCII-код символа имеет то же байтовое значение, что и при кодировании в UTF-8. Иначе говоря, допустимый ASCII-текст автоматически является допустимым текстом в кодировке UTF-8.

Во-вторых, поскольку Unicode-текст, закодированный в UTF-8, — это просто последовательность восьмибитовых байтов, исчезает проблема с порядком следования байтов (endianness). Кодировка UTF-8 (в отличие от UTF-16) по своей природе нейтральна к порядку следования байтов. Это важная особенность при обмене текстом между разными вычислительными системами, которые могут иметь разные аппаратные архитектуры с разным порядком следования байтов.

Если вспомнить о ранее упомянутых двух Unicode-символах, то заглавная буква “C” (кодовая точка U+0043) кодируется в UTF-8 одним байтом 0x43 (значение 43 шестнадцатеричное), который точно соответствует ASCII-коду, связанному с этим символом (поскольку UTF-8 обратно совместима с ASCII). Напротив, японский иероглиф 学 (кодовая точка U+5B66) кодируется в UTF-8 как последовательность из трех байтов: 0xE5 0xAD 0xA6.

UTF-8 — самая популярная Unicode-кодировка в Интернете. Согласно последним статистическим сведениям W3Techs, доступным по ссылке bit.ly/1UT5EBC, UTF-8 используется на 87% всех проанализированных веб-сайтов.

UTF-16 фактически является стандартом де-факто кодировки, используемой Windows API-функциями с поддержкой Unicode. UTF-16 — “родная” Unicode-кодировка и во многих других программных системах. Так, Qt, Java, библиотека International Components for Unicode (ICU) и прочие используют кодировку UTF-16 для хранения Unicode-строк.

В UTF-16 применяются 16-битные кодовые единицы. Как и UTF-8, UTF-16 позволяет кодировать все возможные в Unicode кодовые точки. Однако, если UTF-8 кодирует каждую допустимую в Unicode кодовую точку, используя от одного до четырех восьмибитовых байтов, UTF-16 в каком-то смысле проще. По сути, кодовые точки Unicode кодируются в UTF-16 всего одним или двумя 16-битными кодовыми единицами. Но наличие кодовых единиц размером больше одного байта влечет за собой проблемы с порядком следования байтов: фактически существует и “остроконечная” (big-endian) (от младшего байта к старшему) UTF-16, и “тупоконечная” (little-endian) (от старшего байта к младшему) UTF-16. Тогда как кодировка UTF-8 только одна, поскольку нейтральна к порядку следования байтов.

В Unicode определена концепция плоскости (plane) как непрерывной группы 65 536 (216) кодовых точек. Первая плоскость идентифицируется как плоскость 0, или Basic Multilingual Plane (BMP). Символы почти для всех современных языков и многие знаки находятся в BMP, а все эти BMP-символы представляются в UTF-16 одной 16-битной кодовой единицей.

Дополнительные символы (supplementary characters) расположены в плоскостях, отличных от BMP; они включают пиктографические символы вроде Emoji и символы вышедших из употребления письменностей наподобие египетских иероглифов. Эти дополнительные символы вне BMP кодируются в UTF-16 двумя 16-битными кодовыми единицами, также известными как суррогатные пары (surrogate pairs).

Заглавная буква “C” (U+0043) кодируется в UTF-16 как одна 16-битная кодовая единица 0x0043, а иероглиф 学 (U+5B66) — как одна 16-битная кодовая единица 0x5B66. Для многих Unicode-символов существует прямое соответствие между их абстрактным представлением в виде кодовой точки (скажем, U+5B66) и их шестнадцатеричной кодировкой в UTF-16 (например, 16-битное слово 0x5B66).

Чтобы слегка поразвлечься, давайте посмотрим на некоторые пиктографические символы. Unicode-символ “снеговик” (U+2603, U+2603) кодируется в UTF-8 как трехбайтовая последовательность: 0xE2 0x98 0x83; однако его эквивалент в UTF-16 является одной 16-битной кодовой единицей 0x2603. Unicode-символ “кружка пива” (U+1F37A, U+1F37A), находящийся вне BMP, кодируется в UTF-8 как четырехбайтовая последовательность: 0xF0 0x9F 0x8D 0xBA. Его эквивалент в UTF-16 использует две 16-битные кодовые единицы, 0xD83C 0xDF7A, которые являются примером суррогатной пары в UTF-16.

Преобразования между UTF-8 и UTF-16 через Win32 API

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

Сравнительно недавно стало общепринятым хранить Unicode-текст, закодированный в UTF-8, в экземплярах класса std::string в кросс-платформенном C++-коде. Более того, существует принципиальное соглашение о том, что UTF-8 является лучшей кодировкой для обмена текстом через границы приложений и разные аппаратные платформы. Тот факт, что UTF-8 нейтральна к порядку следования байтов, сыграл важную роль в этом. В любом случае преобразования между UTF-8 и UTF-16 необходимы по крайней мере на границе Win32 API, поскольку Windows-функции с поддержкой Unicode использую UTF-16 в качестве “родной” кодировки.

Теперь углубимся в код на C++, который реализует эти преобразования между Unicode-кодировками UTF-8 и UTF-16. Для этой цели можно использовать две ключевые Win32-функции: MultiByteToWideChar и симметричную ей WideCharToMultiByte. Первую вызывают для преобразования из UTF-8 (“многобайтовой” строки в специфической терминологии Win32 API) в UTF-16 (“широкобайтовую” строку), а вторую — для обратной задачи. Поскольку эти Win32-функции имеют сходные интерфейсы и шаблоны использования, в этой статье я сосредоточусь только на MultiByteToWideChar, но в сопутствующий компилируемый C++-код включена и другая функция.

Применение стандартных строковых STL-классов для хранения Unicode-текста               Поскольку это статья по C++, справедливо ожидать хранения Unicode-текста в одном из строковых классов. Теперь возникает вопрос: какой вид строковых C++-классов можно использовать для хранения Unicode-текста? Ответ зависит от конкретной кодировки Unicode-текста. Если применяется кодировка UTF-8, то, поскольку она основана на восьмибитовых кодовых единицах, для представления каждой из этих единиц в C++ годится простой char. В этом случае STL-класс std::string, основанный на char, — хороший вариант для хранения Unicode-текста в кодировке UTF-8.

С другой стороны, если Unicode-текст закодирован в UTF-16, каждая кодовая единица представляется 16-битными словами. В Visual C++ тип wchar_t имеет как раз размер в 16 бит; соответственно STL-класс std::wstring, основанный на wchar_t, отлично подходит для хранения Unicode-текста в кодировке UTF-16.

Стоит отметить, что стандарт C++ не определяет размер типа wchar_t, поэтому, хотя компилятор Visual C++ использует для него 16 битов, другие компиляторы C++ могут оперировать другими размерами. И фактически размер wchar_t, определяемый компилятором GNU GCC C++ в Linux, составляет 32 бита. Так как тип wchar_t имеет разные размеры в разных компиляторах и платформах, класс std::wstring, основанный на этом типе, не является портируемым. Иначе говоря, wstring можно использовать для хранения Unicode-текста, закодированного в UTF-16, в Windows с компилятором Visual C++ (где размер wchar_t равен 16 битам), но не в Linux с компилятором GCC C++, который определяет 32-битный тип wchar_t.

На самом деле существует еще одна Unicode-кодировка, менее известная и реже используемая на практике, чем ее родственники: UTF-32. Как и предполагает ее название, она основана на 32-битных кодовых единицах. Поэтому 32-битный wchar_t из GCC/Linux — хороший кандидат для кодировки UTF-32 на платформе Linux.

Этой неоднозначностью размера wchar_t определяется отсутствие портируемости C++-кода, оперирующего с этим типом (включая сам класс std::wstring). С другой стороны, std::string, основанный на char, является портируемым. Но с практической точки зрения, следует отметить, что применение wstring для хранения текста в кодировке UTF-16 прекрасно подходит в C++-коде, специфичном для Windows. По сути, эти части кода уже взаимодействуют с Win32 API, которые специфичны для платформы просто по определению. Поэтому добавление wstring в смесь никак не меняет ситуацию.

Наконец, важно отметить, что из-за варьируемой длины в кодировках UTF-8 и UTF-16 возвращаемые значения методов string::length и wstring::length, как правило, не соответствуют количеству Unicode-символов (или кодовых точек), хранимых в строках.

Интерфейс функции преобразования     Давайте разработаем функцию для преобразования Unicode-текст, закодированного в UTF-8, в эквивалентный текст с кодировкой UTF-16. Она может оказаться удобной, например когда у вас есть какой-то кросс-платформенный C++-код, который хранит Unicode-строки в кодировке UTF-8, используя STL-класс std::string, и вы хотите передавать этот текст в Win32-функции с поддержкой Unicode, обычно использующие кодировку UTF-16. Поскольку этот код взаимодействует с Win32 API, он уже не является портируемым, так что в данном случае прекрасно подходит std::wstring. Возможный прототип функции выглядит так:

std::wstring Utf8ToUtf16(const std::string& utf8);

Эта функция преобразования принимает в качестве ввода Unicode-строку в кодировке UTF-8, которая хранится в стандартном STL-классе std::string. Поскольку это входной параметр, он передается в функцию по const-ссылке (const &). После преобразования возвращается строка, закодированная в UTF-16, хранящаяся в экземпляре std::wstring. Однако при конвертации Unicode-кодировок вполне возможны проблемы. Например, входная строка UTF-8 может содержать недопустимую для UTF-8 последовательность (что может быть результатом ошибки в других частях кода или неких злонамеренных действий). В таких случаях самое лучшее с точки зрения безопасности — прекращать преобразование как неудавшееся вместо использования потенциально опасной последовательности байтов. Функция преобразования может обрабатывать случаи с недопустимыми входными последовательностями UTF-8, генерируя C++-исключение.

Определение класса исключения для ошибок преобразования  Какого рода C++-класс можно применить для генерации исключения в случае неудачного преобразования Unicode-кодировки? Как вариант можно было бы использовать класс, уже определенный в стандартной библиотеке, например std::runtime_error. Однако я предпочитаю определить новый пользовательский C++-класс исключения для этой цели, унаследовав его от std::runtime_error. Когда Win32-функции вроде MultiByteToWideChar выдают ошибку, есть возможность вызывать GetLastError, чтобы получить подробную информацию о причине неудачи. Так, в случае недопустимых последовательностей UTF-8 во входной строке типичный код ошибки, возвращаемый GetLastErorr, — ERROR_NO_UNICODE_TRANSLATION. Имеет смысл добавить эту часть информации в пользовательский C++-класс исключения; она может оказаться полезной при отладке. Определение этого класса исключения начинается с:

// utf8except.h
#pragma once

#include <stdint.h>   // для uint32_t
#include <stdexcept>  // для std::runtime_error

// Представляет ошибку при преобразованиях кодировки UTF-8
class Utf8ConversionException
  : public std::runtime_error
{
  // Код ошибки от GetLastError()
  uint32_t _errorCode;

Заметьте, что значение, возвращаемое GetLastError, имеет тип DWORD, который представляет 32-битное целое без знака. Однако DWORD является не портируемым typedef, специфичным для Win32. Но, даже если этот C++-класс исключения генерируется из Win32-специфичных частей C++-кода, он может быть захвачен кросс-платформенным C++-кодом! Поэтому имеет смысл использовать портируемые typedef вместо специфичных для Win32; uint32_t — пример такого типа.

Затем можно определить конструктор, который инициализирует экземпляры этого пользовательского класса исключения сообщением об ошибке и ее кодом:

public:
  Utf8ConversionException(
    const char* message,
    uint32_t errorCode
  )
    : std::runtime_error(message)
    , _errorCode(errorCode)
  { }

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

uint32_t ErrorCode() const
  {
    return _errorCode;
  }
}; // класс исключения

Поскольку этот класс наследует от std::runtime_error, можно вызвать метод what, чтобы получить сообщение об ошибке, переданное в конструктор. Заметьте, что в определении этого класса используются лишь портируемые стандартные элементы, так что этот класс отлично работает в кросс-платформенных частях C++-кода, даже находящихся далеко от специфичной для Windows точки генерации.

Преобразование из UTF-8 в UTF-16: MultiByteToWideChar в действии

Теперь, когда определен прототип функции преобразования и реализован пользовательский C++-класс исключения для корректного представления ошибок преобразования UTF-8, пора создавать тело этой функции. Как и ожидалось, преобразование из UTF-8 в UTF-16 можно выполнить с помощью Win32-функции MultiByteToWideChar. Термины “многобайтовый” и “широкосимвольный” имеют исторические корни. Изначально эта API-функция и симметричная ей WideCharToMultiByte предназначались в основном для преобразования в Win32-функциях с поддержкой Unicode между текстом, хранящемся в специфических кодовых страницах, и Unicode-текстом, который использует кодировку UTF-16. Широкий символ относится к wchar_t, поэтому он связан со строкой на основе wchar_t, которая является строкой в кодировке UTF-16. Многобайтовая строка, напротив, — это последовательность байтов, выраженная в кодовой странице. Устаревшая концепция кодовой страницы была потом расширена для включения кодировки UTF-8.

Типичный шаблон использования этой API-функции заключается в том, что сначала вы вызываете MultiByteToWideChar, чтобы получить размер конечной строки. Затем создаете некий строковый буфер с этим размером. Обычно это делается вызовом метода std::wstring::resize в случае, если в нем будет храниться строка в кодировке UTF-16. (Подробнее об этом см. мою статью “Using STL Strings at Win32 API Boundaries” за июль 2015 года по ссылке msdn.com/magazine/mt238407.) Наконец, функция MultiByteToWideChar вызывается во второй раз для выполнения преобразования кодировки, используя ранее созданный строковый буфер. Заметьте, что тот же шаблон применяется и к симметричной API-функции WideCharToMultiByte.

Давайте реализуем этот шаблон в коде на C++, в теле пользовательской функции Utf8ToUtf16. Начнем с обработки особого случая с пустой входной строкой, где возвращается пустая выходная строка типа wstring:

#include <Windows.h> // для Win32-функций
#include <string>    // для std::string и std::wstring

std::wstring Utf8ToUtf16(const std::string& utf8)
{
  std::wstring utf16; // результат

  if (utf8.empty())
  {
    return utf16;
  }

Флаги преобразования MultiByteToWideChar можно вызвать в первый раз для получения размера конечной строки в UTF-16. Эта Win32-функция имеет сравнительно сложный интерфейс, и ее поведение определяется согласно флагам. Поскольку эта API-функция будет вызываться в теле функции преобразования Utf8ToUtf16 дважды, хорошая практика для надежности кода и удобства его сопровождения — определять именованную константу, которая может использоваться в обоих вызовах:

// Надежный провал, если во входной строке обнаружен
// недопустимый символ UTF-8
constexpr DWORD kFlags = MB_ERR_INVALID_CHARS;

Безусловный провал преобразования, если во входной строке обнаруживается недопустимая для UTF-8 последовательность, является хорошей практикой и с точки зрения безопасности. Применение флага MB_ERR_INVALID_CHARS также приветствуется в книге Майкла Говарда (Michael Howard) и Дэвида Лебланка (David LeBlanc) “Writing Secure Code, Second Edition” (Microsoft Press, 2003).

Если ваш проект использует более старую версию компилятора Visual C++, которая не поддерживает ключевое слово constexpr, вы можете заменить его на static const в этом контексте.

Длины строк и безопасные преобразования из size_t в int              MultiByteToWideChar ожидает, что параметр с длиной входной строки выражен типом int, тогда как метод length в строковых STL-классах возвращает значение типа, эквивалентного size_t. В 64-разрядных сборках компилятор Visual C++ генерирует предупреждение, указывающее на потенциальную опасность потери данных при преобразовании из size_t (размер 8 байтов) в int (размер 4 байта). Но даже в 32-разрядных сборках, где size_t и int определяются компилятором Visual C++ как 32-битные целые, имеется несовпадению “без знака/со знаком”: size_t — беззнаковый тип, а int — со знаком. Это не проблема для строк разумной длины, но для гигантских строк с размером более 231–1, т. е. длиной свыше двух миллиардов байтов, преобразование из целого без знака (size_t) в целое со знаком (int) может генерировать отрицательное число, а отрицательные длины не имеют смысла.

Поэтому вместо простого вызова utf8.length для получения длины входной строки UTF-8 и передачи этого значения в MultiByteToWideChar лучше проверить реальный размер size_t, убедившись, что преобразование в int будет безопасным и имеющим смысл, и только потом передавать его в MultiByteToWideChar.

Следующий код позволяет проверить, что длина size_t не превысит максимальное значение для переменной типа int, и сгенерировать исключение, если такое превышение есть:

if (utf8.length() > static_cast<size_t>(
  std::numeric_limits<int>::max()))
{
  throw std::overflow_error(
    "Input string too long:
    size_t-length doesn't fit into int.");
}

Обратите внимание на использование шаблона класса std::numeric_limits (из стандартного заголовочного файла <limits> в C++) для запроса наибольшего возможного значения для типа int. Однако этот код на самом деле может не скомпилироваться. В чем дело? Проблема в определении макросов min и max в заголовочных файлах Windows Platform SDK. В частности, специфичное для Windows определение макроса max препроцессора конфликтует с вызовом функции-члена std::numeric_limits<int>::max. Предотвратить это можно несколькими способами.

Возможное решение — указать #define NOMINMAX до включения <Windows.h>. Это исключит определение специфичных для Windows макросов препроцессора min и max. Однако отсутствие определения этих макросов на самом деле может вызвать проблемы с другими заголовочными файлами Windows такими как <gdiplus.h>, в котором нужны определения этих специфичных для Windows макросов.

Поэтому другой вариант — использовать дополнительную пару скобок вокруг вызова функции-члена std::numeric_limits::max, чтобы исключить раскрытие вышеупомянутых макросов:

if (utf8.length() > static_cast<size_t>((
  std::numeric_limits<int>::max)()))
{
  throw std::overflow_error(
    "Input string too long:
    size_t-length doesn't fit into int.");
}

Более того, в качестве альтернативы можно было бы использовать константу INT_MAX вместо C++-шаблона класса std::numeric_limits.

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

// Безопасное преобразование из size_t (длина STL-строки)
// в int (для Win32-функций)
const int utf8Length = static_cast<int>(utf8.length());

Заметьте, что длина строки UTF-8 измеряется восьмибитовыми единицами символов (char units), т. е. в байтах.

Первый API-вызов: получение длины конечной строки Теперь MultiByteToWideChar можно вызвать в первый раз, чтобы получить длину конечной строки UTF-16:

const int utf16Length = ::MultiByteToWideChar(
  CP_UTF8,     // исходная строка UTF-8
  kFlags,      // флаги преобразования
  utf8.data(), // указатель на исходную строку UTF-8
  utf8Length,  // длина исходной строки UTF-8 в символах
  nullptr,     // не используется – на этом этапе
               // преобразование не выполняется
  0            // запрос размера буфера назначения в wchar_t
);

Заметьте, что функция вызывается с передачей нуля в качестве последнего аргумента. Это инструктирует MultiByteToWideChar просто вернуть необходимый размер строки назначения; на этом этапе никакое преобразование не выполняется. Также обратите внимание на то, что размер строки назначения выражается в wchar_t (не в восьмибитовых символах); это имеет смысл, так как строка назначения является Unicode-строкой в кодировке UTF-16, состоящей из последовательностей 16-битных wchar_t.

Чтобы получить доступ только для чтения к содержимому входной строки UTF-8 типа std::string, вызывается метод std::string::data. Поскольку длина строки UTF-8 явным образом передается как входной параметр, этот код будет работать и для экземпляров std::string со встроенными NUL.

Константа CP_UTF8 используется, чтобы указать кодировку входной строки в UTF-8.

Обработка ошибок         Если предыдущий вызов функции закончился неудачей, например из-за наличия недопустимых для UTF-8 последовательностей во входной строке, то MultiByteToWideChar возвращает 0. В этом случае можно вызвать Win32-функцию GetLastError, чтобы выяснить детали о причине сбоя. Типичный код ошибки, возвращаемый в случае недопустимых для UTF-8 символов, — ERROR_NO_UNICODE_TRANSLATION.

При неудаче следует сгенерировать исключение. Им может быть экземпляр ранее определенного пользовательского класса Utf8ConversionException:

if (utf16Length == 0)
{
  // Ошибка преобразования: захватываем код ошибки
  // и генерируем исключение
  const DWORD error = ::GetLastError();
  throw Utf8ConversionException(
    "Cannot get result string length when converting " \
    "from UTF-8 to UTF-16 (MultiByteToWideChar failed).",
    error);
}

Выделение памяти для строки назначения          Если вызов Win32-функции выполняется успешно, необходимая длина строки назначения содержится в локальной переменной utf16Length, поэтому можно выделить память под выходную строку UTF-16. Для строк UTF-16, Хранящихся в экземплярах класса std::wstring, достаточно вызова метода resize:

utf16.resize(utf16Length);

Заметьте: поскольку длина входной строки UTF-8 была явно передана в MultiByteToWideChar (вместо передачи –1 и запроса к этой API-функции на сканирование всей входной строки, пока не встретится завершающий NUL-символ), она не станет добавлять дополнительный завершающий NUL-символ к конечной строке. Эта API-функция будет просто обрабатывать точное количество символов во входной строке, указанное явно переданным значением длины. Следовательно, нет нужды вызывать std::wstring::resize со значением “utf16Length + 1” : так как дополнительный завершающий NUL-символ не дописывается в Win32 API, вам незачем оставлять пространство для него в строке назначения типа std::wstring (подробнее об этом см. всю ту же статью за июль 2015 года).

Второй API-вызов: выполнение преобразования               Теперь, когда экземпляр UTF-16 wstring имеет достаточно пространства для конечного текста в кодировке UTF-16, можно вызвать MultiByteToWideChar во второй раз, чтобы получить реально преобразованные биты в строке назначения:

// Преобразование из UTF-8 в UTF-16
int result = ::MultiByteToWideChar(
  CP_UTF8,     // исходная строка UTF-8
  kFlags,      // флаги преобразования
  utf8.data(), // указатель на исходную строку UTF-8
  utf8Length,  // длина исходной строки UTF-8 в символах
  &utf16[0],   // указатель на буфер назначения
  utf16Length  // размер буфера назначения в wchar_t
);

Обратите внимание на использование синтаксиса “&utf16[0]” , чтобы получить доступ для записи к внутреннему буферу памяти std::wstring (это тоже уже обсуждалось в статье за июль 2015 года).

Если первый вызов MultiByteToWideChar был успешен, вряд ли второй вызов потерпит неудачу. Тем не менее, проверка возвращаемого API-функцией значения определенно является хорошей практикой безопасного кодирования:

if (result == 0)
{
  // Ошибка преобразования: захватываем код ошибки
  // и генерируем исключение
  const DWORD error = ::GetLastError();
  throw Utf8ConversionException(
    "Cannot convert from UTF-8 to UTF-16 "\
    "(MultiByteToWideChar failed).",
    error);
}

Иначе (в случае успеха) конечную строку UTF-16 можно наконец вернуть вызвавшему:

return utf16;

} // конец Utf8ToUtf16

Пример использования Итак, если у вас есть Unicode-строка в кодировке UTF-8 (скажем, полученная от какого-то кросс-платформенного C++-кода) и вы хотите передать ее в Win32-функцию с поддержкой Unicode, то наша функция преобразования может быть вызвана так:

std::string utf8Text = /* ...какой-то текст в UTF-8... */;

// Преобразуем из UTF-8 в UTF-16 на границе Win32 API
::SetWindowText(myWindow, Utf8ToUtf16(utf8Text).c_str());

// Примечание: в Unicode-сборках (Visual Studio по умолчанию)
// SetWindowText раскрывается в SetWindowTextW

Функция Utf8ToUtf16 возвращает экземпляр wstring, содержащий строку UTF-16, и применительно к этому экземпляру вызывается метод c_str, чтобы получить исходный указатель в стиле C на строку, завершаемую символом NUL, которая передается в Win32-функции с поддержкой Unicode.

Очень похожий код можно написать для обратного преобразования из UTF-16 в UTF-8, на этот раз вызывая API-функцию WideCharToMultiByte. Как уже отмечалось, преобразования в Unicode между UTF-8 и UTF-16 выполняются без потерь — ни один символ при преобразовании не теряется.

Библиотека преобразований Unicode-кодировок

В сопутствующий этой статье пакет исходного кода включен пример компилируемого C++-кода. Это повторно используемый код, без ошибок компилируемый в Visual C++ при уровне предупреждений 4 (/W4) в 32- и 64-разрядных сборках. Он реализован как библиотека C++ в виде только заголовочных файлов. По сути, этот модуль преобразования Unicode-кодировок состоит из двух заголовочных файлов: utf8except.h и utf8conv.h. Первый содержит определение C++-класса исключения, используемого для уведомления об ошибке при преобразованиях Unicode-кодировок. Второй реализует собственно функции преобразования Unicode-кодировок.

Заметьте, что utf8except.h содержит только кросс-платформенный C++-код; это делает возможным захват исключения при преобразовании кодировки UTF-8 в любых местах ваших проектов на C++, включая те части кода, которые не специфичны для Windows. Напротив, utf8conv.h содержит C++-код, специфичный для Windows, поскольку он напрямую взаимодействует с границей Win32 API.

Для повторного использования этого кода в ваших проектах просто включайте директивой #include эти заголовочные файлы. Сопутствующий пакет исходного кода содержит дополнительный файл, реализующий некоторые наборы тестов.

Заключение

Unicode является стандартом де-факто для представления текста на любых языках в современном программном обеспечении. Unicode-текст можно кодировать в разнообразных форматах: два наиболее важных из них — UTF-8 и UTF-16. В C++-коде для Windows часто требуется преобразовывать строки между кодировками UTF-8 и UTF-16, так как Win32-функции с поддержкой Unicode используют UTF-16 в качестве “родной” Unicode-кодировки. Текст в кодировке UTF-8 удобно хранить в экземплярах STL-класса std::string, тогда как std::wstring хорошо подходит для хранения текста в кодировке UTF-16 в C++-коде для Windows, ориентированном на компилятор Visual C++.

Win32-функции MultiByteToWideChar и WideCharToMultiByte позволяют выполнять преобразования Unicode-текста между кодировками UTF-8 и UTF-16. Я подробно описал шаблон использования функции MultiByteToWideChar, обернув ее в повторно используемую вспомогательную функцию на современном C++ для выполнения преобразований из UTF-8 в UTF-16. Обратное преобразование следует очень похожему шаблону, и повторно используемый C++-код, реализующий его, доступен в пакете кода, сопутствующем этой статье.


Джованни Диканио (Giovanni Dicanio) — программист со специализацией в области C++ и Windows, автор Pluralsight и обладатель звания Visual C++ MVP. Помимо программирования и создания учебных курсов, с удовольствием помогает другим на форумах и в сообществах, преданных C++. С ним можно связаться по адресу giovanni.dicanio@gmail.com. Также ведет блог на blogs.msmvps.com/gdicanio.

Выражаю благодарность за рецензирование статьи экспертам Дэвиду Крейви (David Cravey) и Марку Грегуа (Marc Gregoire).