Некоторые время назад возникла задача проверить совместимость компиляторов с исходными файлами в представлении UTF-8 с маркером и без него. Маркер BOM был придуман для индикации порядка байт в машинных словах. Несмотря на то, что UTF-8 — это байтовый поток и такой маркер не имеет большого смысла в его первоначальном понимании, многие утилиты корректно работают только при наличии маркера в файле. Для обозначения UTF-8 в начало файла вставляется последовательность EF16, BB16, BF16. Файлы в кодировке UTF-8 без маркера неотличимы от ASCII файлов, если используются только 7-битные символы.
В результате эксперимента исследовались файлы в вариантах Unicode (UTF-8 with signature) – Codepage 65001 и Unicode (UTF-8 without signature) – Codepage 65001 для Visual Studio 2013, Visual Studio 2008 и Visual Studio 2014 CTP2. Для полноты сравнения в системе Linux была проведена аналогичная проверка с компилятором GNU C++ 4.7.2. В качестве редактора в Linux использовался vi с опциями set [no]bomb для получения файлов с маркером и без него.
В качестве входного файла использовался следующий исходный код:
#include <iostream> #include <iomanip> #include <string> int main() { using namespace std; wstring wa = L"grüßen"; string a = "grüßen"; cout << hex << uppercase << setfill('0'); cout << "char: "; const size_t asize = a.size() * sizeof(string::value_type); const char* abuf = reinterpret_cast<const char*>(&a[0]); for (size_t i = 0; i < asize; ++i) { cout << setw(2) << (static_cast<unsigned>(abuf[i]) & 0xFF) << ' '; } cout << endl; cout << "wchar_t: "; const size_t wasize = wa.size() * sizeof(wstring::value_type); const char* wabuf = reinterpret_cast<const char*>(&wa[0]); for (size_t i = 0; i < wasize; ++i) { cout << setw(2) << (static_cast<unsigned>(wabuf[i]) & 0xFF) << ' '; } cout << endl; return 0; }В коде используется текст для инициализации multi-byte последовательности (string a) и для инициализации последовательности wide-characters (wstring wa). Надо отметить, что размер wchar_t сильно зависит от платформы. На Linux размер wchar_t составляет четыре байта, тогда как на Windows его размер равен двум байтам.
В данном примере проверяется корректность инициализации строковых констант из исходных кодов UTF-8. Результаты показаны в таблице ниже. Все некорректные результаты выделены красным цветом.
Описание окружения | Результат |
---|---|
Reference UTF-8 code Reference UTF-16LE | char: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00 |
g++ (Debian 4.7.2-5) 4.7.2 w BOM | char: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 00 00 72 00 00 00 FC 00 00 00 DF 00 00 00 65 00 00 00 6E 00 00 00 |
g++ (Debian 4.7.2-5) 4.7.2 w/o BOM | char: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 00 00 72 00 00 00 FC 00 00 00 DF 00 00 00 65 00 00 00 6E 00 00 00 |
Visual Studio 2008 w BOM | char: 67 72 3F 3F 65 6E (компилятор выдает предупреждение C4566) wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00 |
Visual Studio 2008 w/o BOM | char: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 72 00 13 04 58 04 13 04 5F 04 65 00 6E 00 |
Visual Studio 2013 w BOM | char: 67 72 3F 3F 65 6E (компилятор выдает предупреждение C4566) wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00 |
Visual Studio 2013 w/o BOM | char: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 72 00 13 04 58 04 13 04 5F 04 65 00 6E 00 |
Visual Studio 2014 CTP 2 w BOM | char: 67 72 3F 3F 65 6E (компилятор выдает предупреждение C4566) wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00 |
Visual Studio 2014 CTP 2 w/o BOM | char: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 72 00 13 04 58 04 13 04 5F 04 65 00 6E 00 |
Visual Studio 2010 SP1 и выше without BOM #pragma execution_character_set("utf-8") | char: 67 72 D0 93 D1 98 D0 93 D1 9F 65 6E wchar_t: 67 00 72 00 13 04 58 04 13 04 5F 04 65 00 6E 00 |
Visual Studio 2010 SP1 и выше with BOM #pragma execution_character_set("utf-8") | char: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00 |
Visual Studio 2015 Update 2 with BOM u8"grüßen" L"grüßen" | char u8: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00 |
Visual Studio 2015 Update 2 without BOM u8"grüßen" L"grüßen" | char u8: 67 72 C3 BC C3 9F 65 6E wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00 |
Из результатов видно, что компилятор Visual Studio всегда пытается преобразовать файл в текущую системную кодировку, а потом уже его компилирует. Самый надежный способ этого избежать — это инициализировать константы с помощью численных значений, но это не очень удобно.
Стоит отметить, что начиная с Visual Studio 2010 SP1 поддерживается директива execution_character_set, которая на практике заставляет компилятор воспринимать исходный код как UTF-8 и не преобразовывать его (последний пункт в таблице). Также существует хот-фикс, который вводит поддержку директивы в Visual Studio 2008 SP1. И еще, в Visual Studio 2015 Update 2 CTP ввели новый параметр сборки: /source-charset. Он позволяет задать кодировку для всех файлов, в которых ее не удалось определить автоматически, что означает поддержку файлов без маркера BOM.
Надеюсь, кому-то это исследование окажется полезным. Я придерживаюсь мнения, что локализованные строковые константы все-таки лучше хранить в отдельных файлах с ресурсами, чтобы не зависеть от версии и возможностей компилятора на конкретной платформе.
Ссылки по теме: