Данная статья (и я надеюсь что серия статей) посвящена нестандартным расширениям языков C и C++, которые существуют практически в каждом компиляторе.

Языковые расширения — это дополнительные возможности и фичи языка, не входящие в стандарт, но тем ни менее поддерживаемые компиляторами. Исследовать эти расширения очень интересно — в первую очередь потому, что они возникли не на пустом месте; каждое расширение — результат насущной необходимости, возникавшей у большого числа программистов. А мне интересно вдвойне — поскольку мне нравятся языки программирования и я разрабатываю свой, часто оказывается что многие мои идеи реализованы именно в расширениях языка. Стандарты языков C и C++ развиваются крайне медленно, и порой, читая описание расширений, так и хочется воскликнуть «ну это же очевидно! почему этого до сих пор нет в стандарте?».

Языковые расширения — это такая «серая», теневая область, про которую обычно мало пишут и мало знают. Но именно этим она и и интересна!

Предварительно могу сказать, что будут рассмотрены компиляторы общего назначения gcc, msvs, clang, intel, embarcadero, компиляторы для микроконтроллеров iar и keil, и по возможности многие другие компиляторы. Больше всего расширений в GCC, что не удивительно — свободная разработка способствует воплощению разных языковых фич. К тому же, информация по расширениям GCC вся собрана в одном месте, а информацию по остальным компиляторам придется собирать по крупицам. Поэтому начнем с GCC.

Расширения языка Си


Управляющие операторы и блоки кода как выражения


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

int q = 100 + ({ int y = foo (); int z;
   if (y > 0) z = y;
   else z = - y;
   z; });

Локальные метки


Метки, используемые для оператора goto, по умолчанию имеют область видимости ограниченную функцией. Иногда — например при раскрытии макросов — это небезопасно, и целесообразно ограничить область видимости метки текущим блоком кода. Такие метки требуют предварительного объявления с использованием ключевого слова __label__. Сама метка объявляется обычным образом, но теперь ее область видимости — блок, а не функция.

Метки как значения


Другая интереснейшая и мощнейшая низкоуровневая возможность, связанная с оператором goto — использование меток как значений. Фактически эта возможность существует также только в Ассемблере, где метка — лишь адрес в коде. В GCC однако от специального меточного типа отказались, а для приведения метки к типу void* зачем-то ввели унарный оператор &&. Выглядит весьма красиво и по-хакерски:

static void *array[] = { &&foo, &&bar, &&hack };
goto *array[i];

Надо сказать, что с подачи Дейкстры оператор goto находится в немилости у большинства программистов. Во многих случаях это действительно оправдано, но не стоит забывать, что Си — хакерский язык, а значит в нем действует идеология предпочтения возможностей ограничениям. И если в каком-то специфическом месте, например в ядре операционной системы, потребуется goto, лучше использовать его, чем городить ассемблерные вставки. А способов испортить код или сделать его нечитаемым великое множество, среди которых goto далеко не на первом месте.

Вложенные функции


Лямбда-функции в C++ появились только в C++11. А между тем еще в Турбо Паскале была возможность вкладывать одни функции в другие. С появлением С++ и классов ничего не изменилось — классы можно было вкладывать в функции и другие классы, но функции в функции вкладывать было по прежнему нельзя. GCC исправляет эту досадную асимметрию в языке.

Вложенные функции поддерживают доступ к переменным объемлющих, но в отличие от лямбд С++ не требуют явного указания «замыканий», и в отличие от лямбд из высокоуровневых языков, не организуют такие «замыкания» автоматически. Еще одна любопытная возможность — goto из вложенной функции в объемлющую. Это больше похоже на прообраз бросания исключения.

Перенаправление вызова с переменным числом аргументов в другую функцию


Специальные языковые конструкции, предназначенные для передачи переменного числа аргументов функции в другую функцию с переменным числом аргументов, при этом информация о количестве аргументов не требуется. Как известно, стандартным способом работы с переменным числом аргументов в Си являются макросы va_start(), va_arg(), va_end() и тип va_list. Способ основан на том, что аргументы функций в Си записываются в стек в обратном порядке, а эти макросы просто предоставляют доступ к памяти стека. Но в данном расширении мы явно видим что-то новенькое. Что же это?

void * __builtin_apply_args () — функция выделяет память на стеке и копирует туда аргументы вызывающей функции.

void * __builtin_apply (void (*function)(), void *arguments, size_t size) — функция принимает блок данных созданный с помощью __builtin_apply_args, указатель на функцию и размер стека для нее; внутри формируется вызов функции с переданными аргументами. Возвращает блок данных на стеке, в котором хранится возвращаемое значение, возвращенное из function.

void __builtin_return (void *result) — функция заменяет обычный return (то есть после этого buildin'а код уже не выполняется) и возвращает результат выполнения function, запакованный в result.

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

С некоторых пор появились еще два builtin'a, используемых только в inline-функциях, которые жестко инлайнятся всегда (а не на усмортрение компилятора, как это бывает с обычными inline-функиями).

__builtin_va_arg_pack () представляет весь список неименованных аргументов; этот builtin подставляют непосредственно вместо списка аргументов переменной длины.
__builtin_va_arg_pack_len () возвращает количество неименованных аргументов.

Как можно догадаться из требований обязательности inline, эти builtin'ы работают скорее на этапе компиляции, никаких манипуляций со стеком и т.п. в рантайме не производится.

Оператор typeof


Оператор на этапе компиляции возвращает тип выражения. Аналогичный оператор decltype появился таки в С++ не так давно. Однако напомню, что сейчас мы рассматриваем расширения СИ, а не С++! (хотя они конечно же доступны и в gcc c++)

Сокращенный условный оператор


Выражение:

x ? x : y

может быть сокращено до:

x ? : y

Это удобная форма записи, особенно если x сам по себе — длинное выражение. Кстати, называется такая форма Elvis operator и она отличается от Null coalescing_operator (существующего например в C#) тем, что Elvis operator приводит первый операнд к типу bool и сравнивает с false, а Null coalescing сравнивает операнд строго со специальным значением null.

Типы __int128 и long long


Еще одно очевидное расширение для 128-битных и 64-битных целых чисел. Тип long long стандартизирован как в С так и в С++, для 128-битных чисел стандарта пока нет. Интересно, если будет, то как он будет называться? long long long и unsigned long long long?

complex


Поддержка комплексных чисел любого типа на уровне языка. Не уверен что такие типы имеет смысл вводить в язык, но напомню — это си, здесь нет нативных объектов, конструкторов, шаблонов и прочего (а по сути это шаблонный тип). В языке введена поддержка суффиксов 'i' и 'j' (это одно и то же), операторов __real__ и __imag__, а также набора вспомогательных функций.

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

floating types, half precision


Дополнительные типы с плавающей точкой: __float80, __float128, __fp16.
В самом деле, если открыть стандарт IEEE 754, то окажется что типов несколько больше, чем всем известные float и double (и long double, если кто еще помнит).

Decimal float


Еще один интересный формат чисел с плавающей точкой — по основанию 10, а не 2 (смотрим ссылку выше, эти форматы там тоже есть). Напомню, что классический float и double в некоторых случаях дают забавные погрешности, связанные с тем что внутреннее основание степени равна 2, а текстовая запись чисел — десятичная (то есть по основанию 10). Например, 0.1 + 0.2 != 0.3

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

Hex floats


Это способ записи шестнадцатеричных чисел с плавающей точкой (также связанный с тем, что с помощью десятичной нотации нет возможности записать некоторые числа точно). Вместо буквы 'e', занятой для шестнадцатеричной цифры, для экспоненциальной записи применяется буква 'p'. Как вам например такое число: 0x12c0a34.f09de78p3? По-моему, очень даже по-хакерски.

Fixed point


Числа с фиксированной точкой — еще одно полезное расширение в GCC. На некоторых платформах может не быть FPU, иногда расчеты с фиксированной точкой могут быть быстрее или удобнее. На низком уровне это обычные целые числа, для которых принимается цена разрядов, отличная от общепринятой. Теоретически, можно было бы разрешить любое соотношение целой и дробной частей, но в GCC приняты некоторые конкретные соотношения для основных размеров слов (2, 4 и 8 байт), реализованные в модификаторах типов _Fract и _Accum. К тому же, эта возможность почему-то включена не во все компиляторы, так что проверить эту фичу на практике мне не удалось.

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

Именованные адресные пространства


Очень полезная вещь для архитектур с несколькими адресными пространствами. Например для разных микроконтроллеров. Там есть оперативка, flash, eeprom, все это по несколько банков. И независимые систем адресации для каждого адресного пространства.

Массивы нулевой длины


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

Пустые структуры


В отличие от С++, где такие структуры разрешены официально, в Си это расширение. И в Си их размер (sizeof) действительно равен нулю, в отличие от С++ где это почему-то 1 байт.

Массивы, размер которых определяется во время выполнения


Очевидная вещь. Имеется функция alloca() которая выделяет память на стеке; ее не нужно освобождать. GCC добавляет возможность объявлять таким образом массивы на уровне языка:

void foo(int n) {
  int arr[n];
}

Более того, GCC позволяет объявлять вложенные структуры с полями-массивами переменной длины!

void foo (int n) {
       struct S { int x[n]; };
}

И также функции с массивами переменной длины (где длина указывается в списке аргументов функции):

void foo(int len, char data[len][len]) {
/* ... */
}

А если вам хочется указать длину после массива, то и это можно! GCC вводит специальный синтаксис предварительного объявления переменной в списке аргументов функции, кстати крайне интересный для многих других применений (но это уже отдельная тема):

void foo (int len; char data[len][len], int len) {
/* ... */
}

Макросы с переменным числом аргументов


Такие макросы появились в стандарте C99 и C++11. В GCC они появились раньше. Также поддерживаются некоторые улучшения по отношению к стандартной версии. По сути, макрос с переменным числом параметров — это синтаксис, позволяющий передавать в макрос переменное число аргументов и использовать пакет этих аргументов как единое целое для передачи в другие языковые сущности, поддерживающие переменное число аргументов (функции, другие макросы а в С++ еще и шаблоны). В декларации макроса пакет аргументов обозначается как три точки "...", а в теле — как идентификатор __VA_ARGS__.

Теперь о расширениях. Первое — вместо трех точек и __VA_ARGS__ можно использовать нормальные имена, которые декларируются с тремя точками, а используются без них. Это улучшает читаемость кода, и вообще очень красивая идея сама по себе.

#define LOG(args...) fprintf (stderr, args)

Второе — правильная работа с «завершающими запятыми». При любой кодогенерации (а макросы это тоже кодогенерация) неизбежно возникают ситуации, когда в конце списка каких-либо объектов оказывается запятая. По уму, языкам программирования следует считать эту ситуацию нормальной, но к сожалению большинство языков (и Си в том числе) рассматривают это как ошибку. Поэтому придумали костыли — специальный синтаксис ##__VA_ARGS__, который убирает запятую в том случае если пакет аргументов пустой.

Облегченные правила для переноса строк в препроцессоре


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

Индексация не-lvalue массивов


Сейчас это кажется очевидным, но в С90 почему-то нельзя было индексировать не-lvalue массивы. К счастью, и в С99, и в С++ это возможно.

Арифметика с указателями void* и указателями на функции


Разрешены арифметические операции над такими указателями. Размер адресуемых объектов принимается равным 1 байту (но из этого следует странное следствие: sizeof(void) и sizeof от функционального типа равны 1… что не есть хорошо).

Указатели на массивы с квалификаторами


Тонкости и отличия от стандарта реализации работы с указателями на массивы с квалификаторами (const и другими) в GCC C.

Не константные инициализаторы


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

foo (float f, float g) {
       float beat_freqs[2] = { f-g, f+g };
       /* ... */
}

Составные литералы


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

obj = ((struct foo) {x + y, 'a', 0});
char **tbl = (char *[]) { "x", "y", "z" };

Для таких литералов создаются временные объекты соответствующего типа, которые и участвуют в выражениях; поэтому возможно например такое (казалось бы невозможное, т.к. константа не lvalue):

int i = ++(int) { 1 };

Обозначенные (designated) элементы в списках инициализации


И еще одно красивое расширение списков инициализации — внутри списков можно указывать не только все элементы подряд, но и конкретные элементы, используя синтаксис десигнаторов. Для массивов это унарные квадратные скобки, в которых указывается индекс элемента. Так.

int a[6] = { [4] = 29, [2] = 15 };

эквивалентно:

int a[6] = { 0, 0, 15, 0, 29, 0 };

Можно использовать диапазоны:

int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };

Для структур используется аналогичный синтаксис с унарной точкой:

struct point p = { .y = yvalue, .x = xvalue };

Можно смешивать оба типа десигнаторов, а также в одном списке инициализации использовать как десигнаторы так и просто элементы:

struct point ptarray[10] = { [2].y = yv2, {33,44}, [2].x = xv2, [0].x = xv0 };

Кстати, это расширение не реализовано в C++ и его так и не протащили в стандарт. А жаль, это одно из самых красивых расширений, и одна из вещей которые теперь есть в Си и нет в С++.

Диапазоны в case


Возможность использовать диапазоны (с троеточием) в операторе switch в качестве аргументов case:

switch(c) {
case 'A' ... 'Z': /* ... */ break;
}

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

Приведение к типу объединения любого объекта, являющегося членом объединения


Если имеется объединение:

union foo { int i; double d; };

То можно осуществлять явное приведение типа объектов int и double к типу foo:

 union foo u;
 int x;
 double y;
 u = (union foo) x;
 u = (union foo) y;

Аналогично при передаче аргументов в функцию:

void hack (union foo);
hack ((union foo) x);

Смешивание объявления переменных и кода


Привычнейшая в С++ вещь в C90 оказывается тоже являлась расширением (в С99 ее включили в стандарт).

Атрибуты функций, переменных, типов, меток, перечислений, управляющих операторов


Специальное ключевое слово __attribute__, позволяющее назначать определенные компилятором атрибуты (метаинформацию) различным языковым конструкциям. После ключевого слова в круглых скобках указывается имя атрибута. Атрибуты могут быть самые различные. Некоторые атрибуты общие, другие специфичны для конкретной архитектуры. Атрибуты также могут иметь аргументы, которые указываются в круглых скобках после имени атрибута. Вот некоторые атрибуты (на самом деле их очень много и, возможно, эта тема достойна отдельной статьи).

Атрибуты функций:
noreturn, — фукнция никогда не возвращает управление,
pure — функция без побочных эффектов (значение зависит только от аргументов),
format — имеет аргументы в стиле форматной строки printf;

Атрибуты меток:
unused — метка не используется для перехода с помощью goto.
hot — переход по метке имеет высокую вероятность
cold — переход по метке имеет низкую вероятность

Атрибуты элементов перечисления
deprecated — элемент не рекомендуется к использованию, при его использовании в коде будет предупреждение

Атрибуты операторов
fallthrough — используется в операторе switch/case и ставится вместо break, чтобы указать компилятору что здесь намеренно нет break.

Атрибуты переменных
aligned (N) — указывается

Объявление прототипов совместно с определениями функций в старом стиле


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

Комментарии в стиле C++


Вы не поверите, но вообще говоря это тоже расширение — оставшееся с тех древних времен, когда однострочные комментарии еще не входили в стандарт Си. Да, были и такие времена.

Символ доллара в идентификаторах


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

Символ Escape


Использование ‘\e’ в строковых или символьных литералах для вставки символа . Хотя символа нет в переносимом наборе символов, но судя по всему он требуется чаще чем другие управляющие символы, не перечисленные в POSIX.

Запрос выравнивания полей типа или переменной


Ключевое слово __alignof__ возвращает выравнивание, требуемое для поля в некотором типе или просто для некоторого типа. Выравнивание 1 — по границе байта (самое минимально возможное), 2 — по границе слова, 4 — по границе двойного слова и т.д.

inline-функйции


Это всем известная возможность С++, перенесенная в Си.

Использование volatile


Некоторые особенности использования volatile в GCC. Из любопытного — если в коде встречается такое:

volatile int *ptr;
/*...*/
*ptr;

то GCC интерпретирует это как чтение из памяти, на которую указывает ptr, и генерирует соответствующий код

Использование ассемблерных вставок


Рассмотрены вопросы использования ассемблерных вставок в GCC; сама тема ассемблерных вставок, насколько мне известно, не является частью какого-либо стандарта, и достаточно обширна для изложения здесь. Надо сказать, что ассемблерные вставки — это частный случай вставок на другом языке вообще; это точка пересечения двух синтаксисов, которые могут в некотрых случаях даже оказаться несовместимыми. Видимо по этой причине в GCC ассемблерные вставки заключены в кавычки, как строки.

Альтернативные ключевые слова для заголовочных файлов


Какой-то очередной костыль совместимости с разными стандартами. Слова __const__, __asm__, и т.д.

Незавершенные перечисления


Можно объявить enum без элементов; объявлять переменные такого типа нельзя, но можно объявлять указатели на такие перечисления. Сделано для возможности предварительного объявления имени перечисления, по аналогии со структурами и объединениями.

Имена функций в виде строк


Это зачатки рефлексии в Си. Идентификаторы __FUNCTION__ (или __func__) и __PRETTY_FUNCTION__ содержат имя той функции, в которой они используются. __PRETTY_FUNCTION__ содержит расширенное имя — сигнатуру.

Получение адресов вызывающих функций и стековых фреймов


Реализация доступа к стеку вызовов функций. Специальные встроенные функции (built-in'ы) используются для получения адресов возврата нужного уровня (вызывающей функции, функции вызвавшей вызывающую и т.д.), а также адреса стековых фреймов функции и всех вызывающих фунцкий.

Расширения для векторных инструкций


Многие процессоры поддерживают векторные типы данных и инструкции (например SIMD — single instruction, multiple data).

Данное расширение поддерживает объявление векторных типов (с помощью атрибута vector_size) и далее использование обычных операций над этими типами. Типы по сути являются массивам, но операции над ним подобны операциям над обычными переменными. Тем ни менее доступна индексация как в массиве.

typedef int v4si __attribute__ ((vector_size (16)));
v4si a = {1,2,3,4};
v4si b = {3,2,1,4};
v4si c;
a = b + 1;    // a = b + {1,1,1,1};
a = 2 * b;    // a = {2,2,2,2} * b;
c = a >  b;    // c = {0, 0,-1, 0}
c = a == b;   // c = {0,-1, 0,-1}

Также доступна специальная встроенная функция __builtin_shuffle для перестановки элементов в векторе или формировании одного вектора из двух согласно индексной маске:

 v4si a = {1,2,3,4};
 v4si b = {5,6,7,8};
 v4si mask1 = {0,1,1,3};
 v4si mask2 = {0,4,2,5};
 v4si res;
 res = __builtin_shuffle (a, mask1);       /* res is {1,2,2,4}  */
 res = __builtin_shuffle (a, b, mask2);    /* res is {1,5,3,6}  */

Специальный синтаксис для offsetof


Оператор offsetof, возвращающий смещение поля в байтах от начала структуры, может быть реализован макросом вида:

offsetof(s, m) (size_t)&(((s *)0)-›m)

Однако, это является неопределенным поведением по стандарту Си (из-за разыменования нуля) и также приводит к различным нежелательным предупреждениям компилятора; поэтому для реализации offsetof используется встроенная функция __builtin_offsetof

Различные встроенные функции (builtins)


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

Например синус sin(). Визуально это функция, ведет себя как функция (можно даже взять адрес) Однако на уровне компилятора эта конструкция преобразуется в одну или несколько ассемблерных команд FPU, а никак не в вызов функции (за исключением случаев эмуляции FPU для тех архитектур, которые не поддерживают плавающую точку). Такие функции называются встроенными (builtin) и генерируются непосредственно компилятором, что позволяет реализовать функциональность, недоступную для библиотечных функций. Сюда относятся функции для атомарного доступа к памяти, проверки арифметических переполнений, расширение Cilk Plus, математиченские функции, множество функций для работы с конкретными платформами и процессорами и т.д.

Прагмы


Прагмы — директивы, предназначенные в общем случае для тонкого управления процессом компиляции непосредственно из исходников; их можно отнести и к препроцессору, и к самому языку (на самом деле мне сложно отнести их куда-то однозначно, да и препроцессор давно уже слился с языком). GCC поддерживает как прагмы общего назначения, так и для конкретных платформ. Тема большая и интересная, также как и builtin'ы, так что возможно она будет рассмотрена в отдельной части.

Безымянные поля структур и объединений


В структурах и объединениях можно объявлять вложенные безымянные структуры и объединения. Поля этих вложенных структур и объединений будут доступны напрямую:

struct {
       int a;
       union {
         int b;
         float c;
       };
       int d;
     } foo;
foo.b = 10;

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

А если в опциях компилятора включить режим расширений Plan9 ("-fplan9-extensions"), то окажется можно делать вещи, доступные сейчас пожалуй лишь в языке Go: встраивание (embedding) одних структур в другие, что по сути является продвинутой версией наследования — в поля структуры можно встроить целиком поля другой структуры и обращаться к ним как к собственным полям структуры, при этом, в отличие от наследования С++, можно точно указать то место, в которое должны встраиваться поля (что немаловажно для низкоуровневых целей).

typedef struct { int a; } s1; // расширение работает только с typedef'ами
 typedef struct { int x; s1; int y; } s2;
 s2 obj;
 obj.a = 10; // доступ к встроенной структуре

Thread-Local переменные


По сути статические переменные потока, хранящиеся в области памяти потока thread-local storage. Если есть такое явление как потоки и TLS, то должно быть и ключевое слово для объявления там переменных.

Двоичные литералы


Одна из простейших и очевиднейших вещей, которая должна была появиться вместе с языком Си в далеких семидесятых. Но не появилась. Для констант используется префикс ‘0b’.

Тут стоит заметить, что для восьмеричных констант стоило бы ввести префикс '0o' даже несмотря на то что есть официальный способ. Способ записи с начальным нулем ужасен.

Расширения gcc с++


Использование volatile


Некоторые особенности использования volatile в GCC C++, отличия от Си.

Ограниченные указатели (из C99)


Ключевое слово restrict позволяет программисту сообщить компилятору, что объявляемый указатель указывает на блок памяти, на который не указывает никакой другой указатель. Гарантию того, что на один блок памяти не будет указывать более одного указателя, даёт программист. При этом оптимизирующий компилятор может генерировать более эффективный код.

В расширении GCC можно также создавать restrict ссылки и применять его для указателя this.

«Неопределенная» (vague) линковка


Некоторые конструкции в С++ требуют места в объектных файлах и могут оказаться одновременно в нескольких единицах трансляции. Это inline-функции, таблицы виртуальных функций (VTables), объекты type_info и результаты инстанцирования шаблонов. GCC поддерживает размещение таких объектов в COMDAT секции объектного файла, что позволяет исключить дубликаты объектов на этапе линковки.

Прагмы interface и implementation


Такие прагмы позволяют явно указать компилятору, является ли объект интерфейсом или реализацией. Дополнительный костыль к «неопределенной линковке».

Инстанцирование шаблонов


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

Извлечение указателя на функцию из указателя на функцию-член класса


Очевидное расширение возможностей, связанных с операциями ‘->*’ и ‘.*’. Если указатель на поле класса на низком уровне представляет собой байтовое смещение этого поля внутри класса, то указатель на метод — это полноценный указатель на функцию, и GCC добавляет возможность приводить тип указателя на метод к обычному указателю на функцию.

Атрибуты С++


Некоторые атрибуты (задаваемые через ключевое слово __attribute__), применимые только для C++. Несколько примеров: abi_tag — способ задания манглинга имен переменных и функций; init_priority — приоритет инициализации для глобальных объектов.

Объявление множества версий функции


Любопытная возможность. С помощью атрибута target можно объявить несколько версий одной и той же функции — в зависимости от особенностей процессора (например наличия поддержки того или иного набора команд, той или иной модели процессора и т.д.). Чем-то похоже на условную компиляцию, но компилируются все функции, а нужная выбирается во время выполнения программы.

Ассоциированные пространства имен


Возможность эквивалентная inline namespace (поэтому в следующих версиях GCC нестандартная реализация будет удалена).

Признаки типов (Type Traits)


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

__is_abstract (type)
__is_base_of (base_type, derived_type)
__is_class (type)
__is_empty (type)
__is_enum (type)
__is_literal_type (type)

Концепты С++


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

Не рекомендованные к использованию или уже удаленные возможности


Тоже интересно. Эти возможности или удалены, или объявлены не рекомендованными к использованию и будут удалены в ближайшее время.

  • G++ позволял виртуальным функциям возвращающим void* быть перегруженными функциями возвращающими другие значения указателей.
  • Операторы минимума и максимума ‘<?’, ‘>?’, ‘<?=’ и ‘>?=’ (надо же, оказывается и такое было)
  • Именованные возвращаемые значения (а жаль что выкинули)
  • Использование списков инициализации с выражением new
  • параметры шаблонов float и complex (тоже непонятно зачем выкинули)
  • implicit typename extension (к сожалению непонятно что имеется в виду)
  • использование аргументов по умолчанию в указателях на функции, typedef'ах функций и других местах, не предусмотренных стандартом
  • использование литералов с плавающей точкой для вычисления значений констант перечислений будет удалено. Безобидная вещь, кому помешала?
  • Инициализация константных полей класса типов с плавающей точкой прямо в объявлении класса; по стандарту там могут быть только целочисленные поля и перечисления.

Обратная совместимость


Некоторые особенности обратной совместимости с предыдущими версиями С++ и С. Эти возможности включаются специальными опциями компилятора.

  • Переменные, объявленные в цикле for, раньше были доступны вне цикла; с помощью опции -fpermissive можно вернуть старое поведение.
  • Старые заголовочные файлы Си не содержали конструкций extern «C»; специальный режим компиляции может рассматривать все такие файлы как файлы языка Си.

На этом пока все


Как видно, некоторые расширения даже сложно назвать расширениями: это или всем известные фичи, или — что еще хуже — костыли, призванные обеспечить совместимость с какими-то древними и унаследованными стандартами, обойти неудачные решения в дизайне языка и т.д.

В то же время другие — поистине жемчужины среди языковых фич, и очень жаль что их не включают в стандарт.
Поделиться с друзьями
-->

Комментарии (54)


  1. sim-dev
    20.11.2016 16:14
    +2

    На сколько я в курсе, атрибут noreturn означает, что функция никогда не возвращает управление, а не результат.


    1. NeoCode
      20.11.2016 16:30

      Да именно так, спасибо, исправил. Всякие exit(), terminate() или безусловно выбрасывающие исключение.


  1. Sirikid
    20.11.2016 17:22

    Раз пошла такая пьянка… Может кто-нибудь объяснить почему VLA — variable-length arrays aka массивы переменной длинны — сделали опциональными в C11?


    1. kibb
      21.11.2016 00:39

      По слухам, потому что MS не хочет их реализовывать, но хочет получить conformity со стандартом для C11.


    1. roir
      21.11.2016 00:39

      Скорее всего это сделали в целях безопасности.
      Secure Coding Standard


    1. TheCalligrapher
      22.11.2016 18:59

      Это было сделано потому, что в компиляторах для embedded платформ эта фича была практически не востребованной, но в тоже время требовала заметных усилий в реализации. Именно для того, чтобы не тратить усилия реализацию ненужной фичи только ради того, чтобы компилятор стал conforming, VLA сделали опциональными.


      1. Antervis
        23.11.2016 05:53

        а в чем концептуальная сложность реализации?


  1. oteuqpegop
    21.11.2016 00:30
    +3

    >И в Си их размер (sizeof) действительно равен нулю, в отличие от С++ где это почему-то 1 байт.
    В C++ у этого достаточно простая мотивация — стандарт требует, чтобы различные однотипные объекты имели строго различные адреса. Если допустить «пустые» объекты вообще (а не в специфичных ситуациях типа empty base class optimization, где от них нет вреда), то может возникнуть проблема, например, когда в массиве таких объектов все элементы будут иметь совпадающие типы и адреса. Тогда не получится проверять их на тождественность простым сравнением адресов, не будет отношения порядка для таких указателей, в общем, такие объекты все равно будут ущербными, без идентичности и с кучей оговорок, а стандарт усложнится.


  1. AnutaU
    21.11.2016 00:36

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


    1. NeoCode
      21.11.2016 00:38

      У них расширения в том числе и для режима компиляции C90 (многие расширения в этом списке именно такие). И кроме того я хотел упомянуть про десигнаторы потому, что они не включены в расширения С++, а хотелось бы :)


  1. smxfem
    21.11.2016 06:44
    +1

    Кайф. Спасибо!
    Некоторые годные фичи не знал.


    1. smxfem
      21.11.2016 08:20
      -2

      Немного не в тему, C++ так же как и Си состоит из указателей, дефолтных типов и условных переходов, все остальное — обертка на уровне синтаксиса с минимальным количеством лишних сущностей в результирующем коде, в том смысле, что существенные нагромождения программист пишет сам(/использует библиотеки), я правильно понимаю?
      Это к тому, что в недавней теме selgjos заявлял, что с++ = си[++], и мне показалось странным, что его не поняли. Заглядывать же смотреть «под капот» самому сейчас желания не достаточно много.


      1. Sirikid
        21.11.2016 08:32

        Если интересна эта тема можно прочитать книжку «Дизайн и эволюция C++», в ней Страуструп достаточно подробно объясняет почему были приняты те или иные решения, в т. ч. почему был заимствован синтаксис Си, хотя это и очевидно, что бы было проще заманивать сишников :) Так то все Тьюринг-полные языки эвивалентны.


        1. smxfem
          21.11.2016 09:22
          +1

          Да, эквивалентны. И при этом являются языками низкого уровня (одинаково низкого уровня), хотя есть выверенные «мудрецами» шаблоны создания сущностей, которые кажутся высокоуровневыми, но только в рамках наших абстракций. С++ почти не ограничивает программиста и все внутренности торчат наружу, поэтому не могу его считать языком высокого уровня и видить в нем принципиальные отличия от си (речь не про полноту).
          В отличие от какой-нибудь джавы, где сразу в усмирительной рубашке рождаешься :)]-<


          1. Sirikid
            21.11.2016 10:19

            Тогда какие языки высокого уровня вы знаете?


            1. smxfem
              21.11.2016 12:15

              В фортране много встроенных функций математических (с++ функции уровня синтаксиса типа typeof — совсем другое), есть комплексные переменные и 2d-массивы, строки какие-то вроде бы были.
              В паскале были строки точно, хоть и байтовой длины, это уже высоко.
              c# — достаточно того, что там память не ручная.
              Маплы всякие…

              В c/c++ нет вообще ничего высокоуровнего, встроенного в язык(реализация компилятора не имеет отношения к стандарту языка). Или что-то упускаю?


              1. Sirikid
                21.11.2016 12:53
                +1

                Спорно, встраивать все что можно в язык это «бедный» подход, характерный для языков с ограниченным набором возможностей. Тот же C++ позволяет описать вам обобщенный тип complex, который сможет работать поверх любого типа поддерживающего необходимые операции (+, -, *, / и для полного счастья 0 — значение по умолчанию). Точно так же он позволит описать обобщенные/шаблонные структуры/классы для 4-, 8- и 16-мерных чисел.


                1. smxfem
                  21.11.2016 13:15

                  Это замечательно, с++ очень мощный. В совокупности с библиотеками им можно пользоваться как сколь угодно высокоуровневым, с минимальными потерями в скорости. По-моему в этом и заключается вся прелесть языка с++. Вседозволенность компенсируется богатыми средствами создания ограничений, но это на уровне синтаксиса и возлагается на пользователя. Сам язык при этом низкоуровневый и позволяет очень многое.


              1. Antervis
                22.11.2016 06:47
                +1

                В c/c++ нет вообще ничего высокоуровнего, встроенного в язык(реализация компилятора не имеет отношения к стандарту языка). Или что-то упускаю?

                в вашем понимании, каким требованиям должен удовлетворять яп, чтобы считаться высокоуровневым?


                1. smxfem
                  22.11.2016 09:56

                  Наличие сущностей в языке, которые не элементарно записываются на машинном языке, то есть «настоящих» абстракций, которые введены в язык не потому что их легко построить для архитектуры процессора, а потому что их посчитал удобными создатель языка (который, возможно, вообще, с другой планеты).
                  Абстракции же языка с/с++ намеренно максимально ограничены архитектурой процессора, что естественно для языка низкого уровня. Хотя в с/с++ можно записывать арифметические выражения одной строкой. Это все-таки высокий уровень, но такая возможность слишком популярна в яп, поэтому не учитываю. Те же виртуальные функции доступны и на ассемблере как массив адресов, с++ позволяет этим пользоваться в удобной форме.

                  Короче ладно, не важно это все.


                  1. Antervis
                    22.11.2016 10:04

                    Наличие сущностей в языке, которые не элементарно записываются на машинном языке

                    std::sort


                    1. smxfem
                      22.11.2016 11:26

                      stl — библиотека


                      1. Antervis
                        22.11.2016 11:32

                        stl = standard template library


                        1. billyevans
                          22.11.2016 12:27

                          Все, что в stl можно вообще не использовать и не линковаться с этим всем. Это не является элементом языка. В этом большой плюс С++.


                  1. MacIn
                    22.11.2016 18:10

                    А «элементарно» — это как? Вот тот же сишный for — он элементарно записывается, или нет? Где граница?


                    1. Sirikid
                      22.11.2016 18:19

                      (Есть мнение что ответа мы не получим)


                    1. smxfem
                      22.11.2016 18:30

                      Элементарно — условный переход с примочками. Не знаю где граница. В моих гуманитарных высерах отсутствует строгость, одни эмоции.


                      1. MacIn
                        22.11.2016 19:06

                        Я просто представляю себе мощный макроассемблер, тот же Fasm — в нем есть if then else, например. Но это — ассемблер, не ЯВУ.

                        условный переход с примочками

                        Ха, так условный переход — это JZ или там LOOP, если уж говорить о цикле. Но это только маленькая часть, самое главное — expression evaluation.


                        1. smxfem
                          22.11.2016 19:21

                          Парсер — это высокоуровневая возможность, но есть почти во всех ЯП, поэтому проигнорил ее. Только не пойму как устройство цикла for связано с парсером выражений. Анализ того, как именно реализовать цикл, перевычислять ли значения и т.д. — это уже оптимизация и относится к реализации компилятора, а не к ЯП.


                          1. MacIn
                            22.11.2016 19:37

                            Это отдельные высказывания, последняя фраза — самостоятельна.

                            Конструкция сишного цикла не ложится элементарно на машинный язык. while — да, это один в один JZ, а for — нет.


  1. smxfem
    21.11.2016 09:21

    del


  1. MrGluck
    21.11.2016 12:44

    Спасибо за статью, очень интересно


  1. MacIn
    21.11.2016 20:52
    -1

    Фактически эта возможность существует также только в Ассемблере, где метка — лишь адрес в коде

    Фактически эта возможность существует в Фортране чуть с самого начала (в стандарте 66 точно была) и называется «вычисляемый goto», так что это не только низкоуровневый прием.


  1. TheCalligrapher
    22.11.2016 17:33

    Ну то есть просто тупо перевели статью из доков GCC…


    Массивы run-time размера, надо заметить, являются расширением только в С++. В языке С это — стандартная фича начиная с С99.


    Отдельно можно заметить, что "метки как значения" присутствовали на заре зарождения языка С — в CRM С.


    1. MacIn
      22.11.2016 18:10

      Оно даже в Бейсике есть.


  1. nikitadanilov
    22.11.2016 20:17

    > А между тем еще в Турбо Паскале была возможность вкладывать одни функции в другие.
    Как уточнение: вложенные функции были в Паскале с самого начала и, вообще, присутствуют почти во всех языках, происходящих от Алгола-60, C здесь как-раз исключение.

    > Однако, это является неопределенным поведением по стандарту Си (из-за разыменования нуля)
    offsetof() NULL не разыменовывает, он выполняет арифметические операции над NULL-pointer, что совершенно законно.


    1. TheCalligrapher
      22.11.2016 22:43
      +1

      Во-первых, применять адресную арифметику к null-указателям в С и С++ строжайше запрещено (undefined behavior). Поэтому о каком "совершенно законно" вы говорите и откуда вы это взяли — совершенно не ясно.


      Во-вторых, "традиционная" реализация offsetof действительно делает разыменование null-указателя. Однако придираться к этому бесполезно. Как реализовано offsetof никого интересовать не должно. На реализацию стандартной библиотеки требования и правила языка С не распространяются. Спецификация языка гарантирует, что это стандартное макро работает — значит оно работает. Формально, стандартная библиотека не реализована на языке С вообще, а реализована на некоем платформенно-зависимом псевдокоде, любые сходства которого с языком С ограничены лишь интерфейсной совместимостью.


      1. ZyXI
        22.11.2016 23:02

        &(*((type *)NULL)) — это вполне законный код, C99 явно оговаривает в сноске к 6.5.3.2. В начале 6.6 также говорится, что для создания адресной константы должен быть использован & и могут быть использованы [], ., ->, & (унарный) и * (унарная), также как и приведения, но при этом к значению объекта не будет производится обращений. Остаётся только вопрос о том, что должно давать приведение адреса к числу, я сейчас не скажу, где это описано.


        1. TheCalligrapher
          22.11.2016 23:47

          К чему вы это приплели? Что такое &(*((type *)NULL)) и к чему это здесь?


          Напоминаю, что речь идет о выражении (size_t)&(((s *)0)-›m), которое, во-первых, содержит запрещенное разыменование нулевого указателя оператором -> и, во-вторых, завязано на физическое значение нулевого указателя на данной платформе.


          Именно по этим двум причинам подобная реализация offsetof допустима только в рамках стандартной библиотеки конкретной платформы. Это не язык С.


          Отдельно стоит заметить, что "создание адресной константы" обязано подчиняться всем остальным требованиям языка. Никакого отдельного разрешения на разыменование null-указателя в контексте "создания адресной константы" язык С не дает.


          Однако средства статического анализа этого обычно "не знают" и начинают орать во все воронье горло на попытку разыменования null-указателя в offsetof. Именно поэтому в GCC приняли решение заменить реализацию offsetof не непрозрачный builtin.


      1. nikitadanilov
        22.11.2016 23:10

        Во-первых, применять адресную арифметику к null pointer стандартом C явно разрешено (см. C99, 6.5.3.2, особенно примечание 74, где явно указано, что даже разыменование нулевого указателя бывает законно). Во-вторых «разыменование null pointer» это применение операции разыменования (unary * operator) к нулевому указателю, а в макросе offsetof() такая операция просто-напросто отсутствует: этот макрос ничего не разыменовывает.


        1. TheCalligrapher
          22.11.2016 23:42

          Во-первых, повторяю еще раз: применять адресную арифметику к null-указателю в С запрещено. Где вы увидели в 6.5.3.2 хоть малейшее упоминание адресной арифметики — мне в упор не ясно. Адресная арифметика и ее правила в С определны в 6.5.6.


          Во-вторых, разыменовывать null-указатель в С запрещено. Пункт 6.5.3.2, на который вы сослались, нигде не содрежит разрешения на разыменование null-указателя. Наоборот 6.5.3.2/1 это запрещает. Пункт 6.5.3.2 лишь говорит, что комбинация операторов & и * может "взаимно аннигилироваться" и тем самым предотвращать запрещенное разыменование.


          В-третьих, "традиционная" хак-реализация offsetof(size_t)&(((s *)0)-›m) — построена на применении оператора -> к null-указателю. Оператор -> — это разыменование указателя.


          В-четветых, работоспособность выражения (size_t)&(((s *)0)-›m) в качестве вычислителя смещения держится еще на том, что нулевой указатель соответствует физическому адресу 0. Такого язык С никогда не гарантировал. Физический адрес, соответствующий нулевому указателю — зависит от платформы. На некоей платформе этоможет быть 0xFFFFFFFF или 0xBAADF00D. На такой платформе такая реализация offsetof работать не будет.


          1. nikitadanilov
            23.11.2016 00:48

            > Наоборот 6.5.3.2/1 это запрещает.
            Где конкретно? Там явно написано «The operand of the unary & operator shall be [...] unary * operator» и никаких ограничений на аргумент * нет, т.е. явно разрешается &(*(X)) для произвольного X.

            Что значит «предотвращает запрещенное разыменование»? Стандарт просто отмечает, примечанием, что результат &(*0) должен быть 0, там ничего не говорится ни про какую «аннигиляцию».

            > Оператор -> — это разыменование указателя.
            Смешались в кучу кони, люди. :-)

            С точки зрения стандарта, -> это не разыменование, семантика -> описана специальным образом (6.5.2.3), но стандарт гарантирует, что при некоторых условиях (X)->Y эквивалентно (*(X)).Y (примечание 69).

            С неформальной точки зрения, offsetof(A, B) это просто константа компиляции, и никакого разыменования, т.е. обращения к памяти, ее вычисление не требует.

            > На такой платформе такая реализация offsetof работать не будет.
            С этим никто не спорит, у offset() есть и гораздо менее экзотические проблемы. И также понятно, что традиционная реализация вызывает undefined behaviour. Но вовсе не из-за отсутствующего там разыменования, а потому, что семантика A->B стандартом не сводится к арифметике над указателями (в отличие от семантики A[B]).


            1. TheCalligrapher
              23.11.2016 01:26

              Я веду речь именно и только о языке С, который определяет унарный * только для случаев, когда операнд указывает на объект или функцию.


              The unary * operator denotes indirection. If the operand points to a function, the result is
              a function designator; if it points to an object, the result is an lvalue designating the
              object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’. If an
              invalid value has been assigned to the pointer, the behavior of the unary * operator is
              undefined.102)

              Сноска 102 однозначно поясняет:


              Thus, &*E is equivalent to E (even if E is a null pointer), and &(E1[E2]) to ((E1)+(E2)). It is
              always true that if E is a function designator or an lvalue that is a valid operand of the unary &
              operator, *&E is a function designator or an lvalue equal to E. If *P is an lvalue and T is the name of
              an object pointer type, *(T)P is an lvalue that has a type compatible with that to which T points.

              Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, an address inappropriately aligned for the type of object pointed to, and the address of an object after the
              end of its lifetime.

              Как вы умудрились не увидеть "аннигиляцию" мне не ясно, ибо в стандарте ясно сказано


              If the operand is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted

              Это именно та самая аннигиляция. Опять же та же самая сноска 102 в первой своей части ясно говорит то же самое.


              Где вы выкопали "семантика -> описана специальным образом" мне в упор не ясно. Даю под запись: семантика -> в С описана тем же образом, что и семантика *: указатель должен указывать на объект.


              The value is that of the named member of the object to which the first expression points

              Null-указатель не является указателем на объект.


              примечание 69)

              Что? Примечание 69 не имеет к этому никакого отношения. Что за "стандарт" вы там читаете?


              С неформальной точки зрения, offsetof(A, B) это просто константа компиляции, и никакого разыменования, т.е. обращения к памяти, ее вычисление не требует.

              В огороде бузина, а в Киеве дядька...


              Никто не говорит, что offsetof(A, B) вдруг как-то требует обращения к памяти. Речь в данном разговоре идет о формальной легальности конструкции (size_t)&(((s *)0)-›m) в пространстве пользовательского кода. Сам offsetof тут вообще собоку припеку. И этк конструкция — нелегальна, ибо содержит применение оператора -> к указателю, не являющемуся указателем на объект.


              1. nikitadanilov
                23.11.2016 02:16

                > Речь в данном разговоре идет о формальной легальности конструкции (size_t)&(((s *)0)-›m)
                Очень жаль, что вы все время об этом разговаривали, потому что я, как как и было написано в моем первом сообщении, говорю о том, что в этой конструкции нет разыменования (т.е. применения unary *), вне зависимости от того, легальна ли вся конструкция.

                Например (приходится повторить), в конструкции A[B] есть разыменование, потому что стандарт явно определяет ее как (*(A+B)). Для конструкции A->B такого сведения нет. Вы, кажется, пользуетесь каким-то своим неформальным пониманием разыменования, не определив его явно.

                > И этк конструкция — нелегальна, ибо содержит применение оператора -> к указателю, не являющемуся указателем на объект.
                Вы можете процитировать соответствующий пункт из списка undefined behaviours в конце стандарта?


                1. TheCalligrapher
                  23.11.2016 04:52

                  Для конструкции A->B такого сведения нет. Вы, кажется, пользуетесь каким-то своим неформальным пониманием разыменования, не определив его явно.

                  Вы, похоже, пытаетесь выкрутиться из ситуации, придираясь к слову "разыменование". Давайте забудем об этом слове. Применение оператора -> к нулевому указателю запрещено и без этого слова. Спецификация оператора -> ясно говорит, что указатель слева от -> должен указывать на объект.


                  Вы можете процитировать соответствующий пункт из списка undefined behaviours в конце стандарта?

                  Нет, навскидку не вижу там такого пункта. Но это не так важно, ибо соответствующий раздел — "informative", а не "normative".


                  1. nikitadanilov
                    23.11.2016 15:28

                    > Вы, похоже, пытаетесь выкрутиться из ситуации, придираясь к слову «разыменование».
                    В моих комментариях, которые вам необходимо перечитать, с самого начала и неоднократно было указано, что я понимаю «разыменование», как применение unary operator *. Каким образом вы его понимаете — до сих пор непонятно.

                    > Но это не так важно, ибо соответствующий раздел — «informative», а не «normative».
                    Есть другие примеры undefined behaviour, которые авторы решили не включать в этот список, или это удивительное исключение?


                1. TheCalligrapher
                  23.11.2016 05:02

                  Отдельно, кстати, стоит заметить, что и на операнд унарного & тоже налагаются строгие требования


                  The operand of the unary & operator shall be either a function designator, the result of a
                  [] or unary * operator, or an lvalue that designates an object that is not a bit-field and is
                  not declared with the register storage-class specifier.

                  Lvalue, полученное через ->, примененный к нулевому указателю, не "designates an object", по каковой причине такое применение унарного & вызывает неопределенное поведение.


                  В случае &*E для нулевого указателя E ситуацию спасает взаимная аннигиляция & и *, как уже говорилось выше. Но в случае &E->member никакой аннигиляции нет и такое применение & — нелегально.


      1. Ivan_83
        23.11.2016 02:02
        -2

        Глупости.
        Замените NULL на любое валидное значение со стёка и это ничего не изменит, за исключением того что придётся вычитать дополнительно.

        Или вот:
        struct s_type *a = NULL;
        size_t m_off = (size_t)&a-›m;
        Но уж если совсем зудёж то можно добавить: m_off -= (size_t)NULL;
        На случай если NULL вдруг не 0.
        Но в оригинале было (size_t)&(((s *)0)-›m) так что можно не парится.

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

        То что авторы гцц понапихали к себе всякого барахла абсолютно ни о чём не говорит, тем паче buildin — это вообще любой школьник туда может барахла накидать, всем плевать на это.

        Я бы конечно предпочёл избавится от этого макроса в пользу реализации в самом компиляторе, но в целом пофик ибо практически не используется, держу только ради 1-2 функций, весьма специфичных для разных ОС.


        1. TheCalligrapher
          23.11.2016 05:06
          +2

          Не надо пытаться непрерывно повторять одни и те же домыслы — они от этого правильнее не станут.


          Если спецификация языка говорит, что поведение не определено — то поведение не определено. И в данном случае оно именно не определено. Конец дискуссии.


          А то, что у какого-нибудь "васипупкина" это "всегда работало" — это не аргумент для данного разговора.


    1. ZyXI
      22.11.2016 22:54

      Где?то в блоге PVS?Studio эта проблема обсуждалась: авторы данного статического анализатора считают, что, во?первых, неопределённое поведение есть, во?вторых, вы вряд ли найдёте более?менее популярный компилятор, где реализованный через такое неопределённое поведение offsetof выдаст неверный результат. Авторы GCC тоже ведь зачем?то сделали __builtin_offsetof, а не оставили макрос с NULL, как, к примеру, авторы tcc или pcc. clang тоже имеет __builtin_offsetof, но они, скорее, сделали всё для совместимости.


      Ещё: не знаю, почему, но Vim вместо offsetof или NULL хака использовал глобальную переменную. Вероятно, «неопределённое поведение» где?то всё же создавало проблемы, а Bram не любит расставаться с поддержкой морально устаревших компиляторов (т.е. тех, что не имеют offsetof макроса).


      1. nikitadanilov
        22.11.2016 23:13

        Обычный offsetof() может привести к undefined behaviour, когда применяется как offsetof(struct foo, array[n]), где n — больше числа задекларированных элементов в поле-массиве. Типичная ситуация: массив нулевой длины в конце структуры, фактически память под массив выделяется при аллокации.


      1. TheCalligrapher
        23.11.2016 00:05
        +1

        Где?то в блоге PVS?Studio эта проблема обсуждалась: авторы данного статического анализатора считают, что, во?первых, неопределённое поведение есть

        Неопределенное поведение в таком коде, если он будет написан в пользовательском пространстве — есть. Этот вопрос не обсуждается.


        Другое дело, что, как я уже сказал выше, этот критерий неприменим к коду, написанному на территории стандартной библиотеки, о чем было в свое время ясно сказано в DR#44 (http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_044.html).


        Понятно, что в случае макроподстановок в общем случае трудно/невозможно отличить, откуда "пришел" код, по каковой причине такая реализация действительно будет отлавливаться анализаторами.


        Реализация через глобальную переменную поможет решить обе проблемы оригинального offsetof: и устранит разыменование null-указателя, и устранит зависимость от физического значения null-указателя.


  1. chabapok
    24.11.2016 10:20

    Кстати, насчет редкоиспользуемости goto. Возьмем, наприемр, mysql:

    $ apt-get source mysql-5.6
    /tmp/1/mysql-5.6-5.6.33$ grep -R goto --include=*.cc --include=*.c --include=*.cpp |wc -l
    6966


    1. Antervis
      24.11.2016 11:07
      +3

      Ну так в си (за неимением деструкторов и raii) ничего лучше goto exit; где после exit очистка ресурсов перед выходом, не придумали