Расширения языков С и С++. Часть 1

в 12:01, , рубрики: C, c++, gcc, Компиляторы, Программирование

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

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

Предварительно могу сказать, что будут рассмотрены компиляторы общего назначения 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»; специальный режим компиляции может рассматривать все такие файлы как файлы языка Си.

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

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

Автор: NeoCode

Источник

Поделиться новостью

* - обязательные к заполнению поля