Как должны работать u8-литералы?

Не удалось понять семантику u8-литералов или, вернее, понять результат на g++ 4.8.1

Это мое ожидание:

const std::string utf8 = u8"åäö"; // or some other extended ASCII characters
assert( utf8.size() > 3);

Это результат на g++ 4.8.1

const std::string utf8 = u8"åäö"; // or some other extended ASCII characters
assert( utf8.size() == 3);
  • Исходным файлом является ISO-8859 (-1)
  • Мы используем эти директивы компилятора: -m64 -std = С++ 11 -pthread -O3 -fpic

В моем мире, независимо от кодировки исходного файла, результирующая строка utf8 должна быть длиннее 3.

Или, я полностью неправильно понял семантику u8 и прецедента? Пожалуйста, просветите меня.

Обновление

Если я явно расскажу компилятору о том, что кодирует исходный файл, как и многие из предложенных, я получил ожидаемое поведение для букв u8. Но, регулярные литералы также кодируются в utf8

То есть:

const std::string utf8 = u8"åäö"; // or some other extended ASCII characters
assert( utf8.size() > 3);
assert( utf8 == "åäö");
  • директива компилятора: g++ -m64 -std = С++ 11 -pthread -O3 -finput-charset = ISO8859-1
  • Пробовал несколько других charset, определенных из iconv, например: ISO_8859-1 и т.д.

Я еще больше смущен, чем раньше...

Ответ 1

Префикс u8 действительно просто означает "при компиляции этого кода, сгенерируйте строку UTF-8 из этого литерала". В нем ничего не говорится о том, как литерал в исходном файле должен интерпретироваться компилятором.

Итак, у вас есть несколько факторов:

  • какая кодировка является исходным файлом, записанным в (в вашем случае, по-видимому, ISO-8859). Согласно этой кодировке строковый литерал "åäö" (3 байта, содержащий значения 0xc5, 0xe4, 0xf6)
  • какая кодировка делает компилятор при чтении исходного файла? (Я подозреваю, что GCC по умолчанию использует UTF-8, но я могу ошибаться.
  • кодировка, которую компилятор использует для сгенерированной строки в объектном файле. Вы указываете, что это UTF-8 через префикс u8.

Скорее всего, №2 - это то, где это происходит неправильно. Если компилятор интерпретирует исходный файл как ISO-8859, он считывает три символа, преобразует их в UTF-8 и записывает их, предоставляя вам 6-байтовое (я думаю, каждый из этих символов кодирует до 2 байтов в UTF -8) в результате.

Однако, если он предполагает, что исходным файлом является UTF-8, тогда вообще не нужно делать преобразование: он считывает 3 байта, который он предполагает, это UTF-8 (хотя они являются недопустимыми мусорами значения для UTF-8), и поскольку вы запросили также, чтобы выходная строка была UTF-8, она просто выводит те же 3 байта.

Вы можете указать GCC, какую исходную кодировку предполагается использовать с помощью -finput-charset, или вы можете закодировать источник как UTF-8, или вы можете использовать escape-последовательности \uXXXX в строковом литерале (\u00E5 вместо å, например)

Изменить:

Чтобы уточнить бит, когда вы указываете строковый литерал с префиксом u8 в исходном коде, вы сообщаете компилятору, что "независимо от того, какую кодировку вы использовали при чтении исходного текста, пожалуйста, преобразуйте его в UTF -8 при записи его в файл объекта". Вы ничего не говорите о том, как интерпретировать исходный текст. Это зависит от того, какой компилятор должен решить (возможно, на основе того, какие флаги вы передали ему, возможно, на основе среды процесса или, возможно, просто с использованием жесткого кодированного значения по умолчанию)

Если строка в исходном тексте содержит байты 0xc5, 0xe4, 0xf6, и вы скажете, что "исходный текст закодирован как ISO-8859", тогда компилятор распознает, что "строка состоит из символов", åäö ". Он увидит префикс u8 и преобразует эти символы в UTF-8, записав байтовую последовательность 0xc3, 0xa5, 0xc3, 0xa4, 0xc3, 0xb6 в объектный файл. В этом случае вы получите действительная кодированная текстовая строка UTF-8, содержащая представление UTF-8 символов "åäö".

Однако, если строка в исходном тексте содержит один и тот же байт, и вы делаете компилятор уверенным, что исходный текст закодирован как UTF-8, тогда есть две вещи, которые может сделать компилятор (в зависимости от реализации:

  • он может попытаться проанализировать байты как UTF-8, и в этом случае он распознает, что "это не допустимая последовательность UTF-8" и выдает ошибку. Это то, что делает Кланг.
  • в качестве альтернативы, он может сказать: "Хорошо, у меня здесь 3 байта, мне сказали предположить, что они образуют правильную строку UTF-8. Я буду следить за ними и посмотреть, что произойдет". Затем, когда предполагается записать строку в объектный файл, она идет "ok", у меня есть эти 3 байта от ранее, которые обозначены как UTF-8. Префикс u8 здесь означает, что я должен писать эта строка как UTF-8. Прохладный, не нужно делать преобразование, тогда я просто напишу эти 3 байта, и я закончил ". Это то, что делает GCC.

Оба действительны. Язык С++ не указывает, что компилятор должен проверить правильность строковых литералов, которые вы передаете ему.

Но в обоих случаях обратите внимание, что префикс u8 не имеет ничего общего с вашей проблемой. Это просто говорит компилятору преобразовать из "любой кодировки, которую имела строка при ее чтении, в UTF-8". Но даже до этого преобразования строка была уже искажена, потому что байты соответствовали символьным данным ISO-8859, но компилятор считал их UTF-8 (потому что вы не говорили об этом иначе).

Проблема, которую вы видите, просто заключается в том, что компилятор не знал, какую кодировку использовать при чтении строкового литерала из вашего исходного файла.

Другая вещь, которую вы замечаете, заключается в том, что "традиционный" строковый литерал без префикса будет закодирован с любой кодировкой, которую любит компилятор. Префикс u8 (и соответствующие префиксы UTF-16 и UTF-32) были введены точно, чтобы вы могли указать, какую кодировку вы хотите, чтобы компилятор записывал вывод. В простых литералах без префикса не указывается кодировка в все, оставляя это до компилятора, чтобы решить одно.

Ответ 2

Чтобы проиллюстрировать это обсуждение, рассмотрим несколько примеров. Рассмотрим код:

int main() {
  std::cout << "åäö\n";
}

1) Компиляция с помощью g++ -std=c++11 encoding.cpp приведет к выполнению исполняемого файла, который дает:

% ./a.out | od -txC
0000000 c3 a5 c3 a4 c3 b6 0a

Другими словами, два байта на "кластер графемы" (согласно южнокодированному жаргону, т.е. в этом случае, на символ) плюс итоговая новая строка (0a). Это связано с тем, что мой файл закодирован в utf-8, предполагается, что ввод-кодировка является utf-8 по cpp, а exec-charset - utf-8 по умолчанию в gcc (см. https://gcc.gnu.org/onlinedocs/cpp/Character-sets.html). Хорошо.

2) Теперь, если я конвертирую свой файл в iso-8859-1 и снова скомпилирую с помощью той же команды, я получаю:

% ./a.out | od -txC
0000000 e5 e4 f6 0a

то есть. три символа теперь кодируются с использованием iso-8859-1. Я не уверен в том, что здесь происходит волшебство, так как на этот раз кажется, что cpp правильно догадался, что файл iso-8859-1 (без подсказки), преобразовал его в utf-8 внутренне (согласно ссылке выше), но компилятор все еще сохранил строку iso-8859-1 в двоичном формате. Это можно проверить, посмотрев раздел .rodata двоичного файла:

% objdump -s -j .rodata a.out

a.out:     file format elf64-x86-64

Contents of section .rodata:
400870 01000200 00e5e4f6 0a00               ..........

(Обратите внимание на последовательность "e5e4f6" байтов).
Это имеет смысл, поскольку программист, который использует латинские буквы 1, не ожидает, что они выйдут в качестве utf-8 строк в своем программном выпуске.

3) Теперь, если я сохраню тот же файл iso-8859-1, но скомпилирую с g++ -std=c++11 -finput-charset=iso-8859-1 encoding.cpp, тогда я получаю двоичный файл, который выводит данные utf-8:

% ./a.out | od -txC
0000000 c3 a5 c3 a4 c3 b6 0a

Я нахожу это странным: исходная кодировка не изменилась, я явно говорю gcc, что это латинский-1, и я получаю utf-8 в результате! Обратите внимание, что это может быть переопределено, если я явно запрашиваю exec-charset с помощью g++ -std=c++11 -finput-charset=iso-8859-1 -fexec-charset=iso-8859-1 encoding.cpp:

% ./a.out | od -txC
0000000 e5 e4 f6 0a

Мне не ясно, как эти два варианта взаимодействуют...

4) Теперь добавьте префикс "u8" в микс:

int main() {
  std::cout << u8"åäö\n";
}

Если файл является utf-8-кодированным, неудивительно компилировать с настройками по умолчанию char -sets (g++ -std=c++11 encoding.cpp), то вывод также является utf-8. Если я попрошу компилятор вместо этого использовать iso-8859-1 (g++ -std=c++11 -fexec-charset=iso-8859-1 encoding.cpp), то вывод будет еще utf-8:

% ./a.out | od -txC
0000000 c3 a5 c3 a4 c3 b6 0a

Итак, похоже, что префикс "u8" помешал компилятору преобразовать литерал в набор символов выполнения. Еще лучше, если я конвертирую один и тот же исходный файл в iso-8859-1 и компилирую с помощью g++ -std=c++11 -finput-charset=iso-8859-1 -fexec-charset=iso-8859-1 encoding.cpp, то я все еще получаю вывод utf-8:

% ./a.out | od -txC
0000000 c3 a5 c3 a4 c3 b6 0a

Итак, кажется, что "u8" фактически действует как "оператор", который сообщает компилятору "преобразовать этот литерал в utf-8".