Ограничения для escape-последовательностей Unicode в C11

Почему существует ограничение для escape-последовательностей Unicode (\unnnn и \Unnnnnnnn) в C11, так что могут быть представлены только те символы, которые не входят в базовый набор символов? Например, следующий код приводит к ошибке компилятора: \u000A is not a valid universal character. (Некоторые сайты со словарями Unicode даже называют этот недопустимый формат каноническим для языков C/C++, хотя, по общему признанию, они, вероятно, сгенерированы автоматически):

static inline int test_unicode_single() {
        return strlen(u8"\u000A") > 1;
}

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


person Benjamin Crawford Ctrl-Alt-Tut    schedule 06.07.2020    source источник
comment
@ЕвгенийШ. Не понимаю, как это относится к моему вопросу. В вопросе используется 0x000A (U + 000A) (LF - перевод строки) в качестве примера символа Unicode, представленного 4 шестнадцатеричными цифрами.   -  person Benjamin Crawford Ctrl-Alt-Tut    schedule 06.07.2020
comment
Как насчет этого: port70.net/~nsz/c/c11/ n1570.html#6.4.3p2 Универсальное имя символа не должно указывать символ, короткий идентификатор которого меньше 00A0, кроме 0024 ($), 0040 (@) или 0060 ('), ни один в диапазоне от D800 до DFFF включительно.   -  person Eugene Sh.    schedule 06.07.2020
comment
@ЕвгенийШ. Это полезно, особенно сноска. Но все равно остается вопрос - почему? Это забота о безопасности?   -  person Benjamin Crawford Ctrl-Alt-Tut    schedule 06.07.2020
comment
Возможно, вы захотите взглянуть на stackoverflow.com/a/20158506.   -  person Stephan Schlecht    schedule 06.07.2020


Ответы (1)


Именно для того, чтобы избежать альтернативных вариантов написания.

Основными причинами добавления универсальных имен символов (UCN) в C и C++ были:

  • разрешить идентификаторам включать буквы за пределами основного исходного набора символов (например, ñ).

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

Кроме того, было желание, чтобы изменения в существующих компиляторах были как можно более ограниченными, и, в частности, чтобы компиляторы (и другие инструменты) могли продолжать использовать свои установленные (и часто сильно оптимизированные) функции лексического анализа.

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

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

  2. Компилятор может внутри использовать строгий (7-битный) ASCII. Входные файлы в кодировках, допускающих другие символы, будут транскрибированы в ASCII, а символы, отличные от ASCII, будут преобразованы в UCN до любого другого лексического анализа.

По сути, обе эти стратегии будут реализованы в фазе 1 (или эквивалентной), то есть задолго до того, как будет проведен лексический анализ. Но обратите внимание на разницу: стратегия 1 преобразует UCN во внутреннюю кодировку символов, а стратегия 2 преобразует непредставимые символы в UCN.

Общим для этих двух стратегий является то, что после завершения транскрипции больше нет никакой разницы между символом, введенным непосредственно в исходный поток (в любой кодировке, используемой исходным файлом), и символом, описанным с помощью UCN. Таким образом, если компилятор разрешает исходные файлы UTF-8, вы можете ввести ñ либо как два байта 0xc3, 0xb1, либо как шестисимвольную последовательность \u00D1, и они оба будут иметь одну и ту же последовательность байтов. Это, в свою очередь, означает, что каждый идентификатор имеет только одно написание, поэтому не требуется никаких изменений (например) для поиска в таблице символов.

Как правило, компиляторы просто передают имена переменных через конвейер компиляции, оставляя их для обработки ассемблерам или компоновщикам. Если эти последующие инструменты не принимают расширенные кодировки символов или UCN (в зависимости от стратегии реализации), то имена, содержащие такие символы, должны быть искажены (транскрибированы), чтобы сделать их приемлемыми. Но даже если это необходимо, это незначительное изменение, и его можно выполнить в четко определенном интерфейсе.

Вместо того, чтобы разрешать споры между поставщиками компиляторов, чьи продукты (или группы разработчиков) имели явные предпочтения между двумя стратегиями, комитеты по стандартам C и C++ выбрали механизмы и ограничения, которые делают обе стратегии совместимыми. В частности, оба комитета запрещают использование UCN, представляющих символы, которые уже имеют кодировку в базовом исходном наборе символов. Это позволяет избежать таких вопросов, как:

  • Что произойдет, если я поставлю \u0022 внутри строкового литерала:

      const char* quote = "\u0022";
    

    Если компилятор преобразует UCN в символы, которые они представляют, то к тому времени, когда лексический анализатор увидит эту строку, "\u0022" будет преобразовано в """, что является лексической ошибкой. С другой стороны, компилятор, который сохраняет UCN до конца, с радостью примет это как строковый литерал. Запрет на использование UCN, представляющего собой кавычки, позволяет избежать этой возможной непереносимости.

  • Точно так же будет ли '\u005cn' символом новой строки? Опять же, если UCN преобразуется в обратную косую черту на этапе 1, то на этапе 3 строковый литерал определенно будет рассматриваться как новая строка. Но если UCN преобразуется в символьное значение только после того, как токен символьного литерала был идентифицирован как таковой, то результирующий символьный литерал будет содержать два символа (значение, определяемое реализацией).

  • А как же 2 \u002B 2? Будет ли это выглядеть как дополнение, хотя UCN не должны использоваться для знаков препинания? Или это будет идентификатор, начинающийся с небуквенного кода?

И так далее, по большому количеству подобных вопросов.

Всех этих подробностей можно избежать благодаря простому приему, требующему, чтобы UCN не могли использоваться для написания символов в основном исходном наборе символов. И это то, что было воплощено в стандартах.

Обратите внимание, что базовый исходный набор символов не содержит всех символов ASCII. Он не содержит большинства управляющих символов и не содержит символов ASCII $, @ и `. Эти символы (которые не имеют значения в программе C или C++ за пределами строковых и символьных литералов) могут быть записаны как UCN \u0024, \u0040 и \u0060 соответственно.

Наконец, чтобы увидеть, какие узлы вам нужно развязать, чтобы правильно лексически анализировать C (или C++), рассмотрим следующий фрагмент:

const char* s = "\\
n";

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

const char* s = "\n";

Но это могло быть неочевидно, глядя на исходный код.

person rici    schedule 06.07.2020