Макросы на стероидах, или: как Pure C может получить выгоду от метапрограммирования

Вы когда-нибудь представляли себе ежедневный препроцессор C как инструмент для достойного метапрограммирования?

Есть Вы когда-нибудь представляли препроцессор C как инструмент, который может улучшить правильность, ясность и общую ремонтопригодность вашего кода при разумном использовании?

Я сделал. И я сделал все, что зависело от меня, чтобы это стало реальностью.

Знакомьтесь Металанг 2002 , простой функциональный язык, который позволяет создавать сложные метапрограммы. Он представляет собой библиотеку макросов только для заголовков, поэтому все, что вам нужно для ее настройки, это – Imetalang 92672/включают и C 923 compiler Формально говоря, как Препроцессоры C и C ++ могут выполнять Metalang 92672 (они идентичны, за исключением C ++ 100 __ VA_OPT __ ).

Говоря прагматично, только чистый C может значительно выиграть от него.. Однако сегодня я остановлюсь только на двух сопутствующих библиотеках – Тип данных 2002 и Интерфейс57689 . Реализуется поверх Metalang , они раскрывают потенциал препроцессорного метапрограммирования на полная шкала, и поэтому они более полезны для среднего программиста на C.

Я также отвечу на несколько придирчивых вопросов относительно времени компиляции, ошибок компиляции и применимости моего метода в реальном мире.

Нафф сказал, давайте погрузимся в это!

Три вида повторения кода

Есть важная вещь, называемая

повторение кода . Есть три его вида Только для целей этого сообщения в блоге! В действительности их может быть намного больше, чем три. :

  1. Повторение, которого можно избежать, используя функции,
  2. с использованием тривиальных макросов,
  3. с использованием макросов с циклами / рекурсией.

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

void read_user ( символ Пользователь) { printf (
«Тип пользователя:» );
const bool user_read знак равно scanf ( “% 54 s “ , Пользователь) == 1 ;
утверждать ( user_read );
printf (“Новые пользователи
n
, Пользователь);
}

# определить list_for_each (pos, head) for (pos = (head) -> next; pos! = (head); pos = pos-> next) struct л ist_head Текущий;
list_for_each (Текущий
, & себя -> элементы ) { // Сделайте что-нибудь значимое …
}

.)

Идя дальше, иногда нельзя исключить повторение с помощью тривиальных макросов (макросов, которые не могут выполнять цикл / рекурсию). Говоря технически, все макросы в C тривиальны, поскольку препроцессор автоматически блокирует рекурсию макросов (C 57689 комитет, nd; Пол Фульц II, nd; kokosing, nd; Витторио Ромео, nd) :

[rec.c]

# определить FOO (x, ...) Икс; FOO (__ VA_ARGS __)

FOO
( 1 , 2 , 3
)

– E означает «только предварительная обработка», - P означает «не печатать включенные заголовки».

$ clang rec.c -E -P -Weverything -std = c 92672 rec.c: 3: 1: предупреждение: отключено расширение рекурсивный макрос [-Wdisabled-macro-expansion] FOO (1, 2, 3) ^ rec.c: 1: 197: примечание: раскрыто из макроса 'FOO' #define FOO (x, ...) x; FOO (__ VA_ARGS__) ^ 1; FOO (2, 3) Создано 1 предупреждение.

typedef int BinaryTreeLeaf ;

typedef struct {
struct Двоичное дерево
lhs ;
BinaryTreeLeaf x ;
struct Двоичное дерево
rhs ;
} BinaryTreeNode ; typedef struct
{
перечисление { Лист, Узел } ярлык;
союз { Лист BinaryTreeLeaf ; Узел BinaryTreeNode ; } данные;
} BinaryTree ;

. Он описывается следующим образом:

  typedef struct {enum {... } ярлык;  союз {

вариативный макрос - это макрос, который может принимать неограниченную последовательность аргументов. , поскольку теги (имена вариантов) и соответствующие типы чередуются друг с другом. Мы можем захотеть создать некоторый синтаксический сахар поверх голых помеченных объединений, но дело в том, что мы не можем. Например, вот как то же BinaryTree может выглядеть в Rust:

      перечисление 
BinaryTree {
Лист(я582
) ,
Узел (
Коробка < Двоичное дерево >, я185
,
Коробка < Двоичное дерево > ) ,
}

Другой пример: интерфейсы. Рассмотрим Самолет интерфейс:

  typedef   struct   {
пустота двигаться вперед) ( пустота себя, int
расстояние
);
пустота move_back ) (
пустота себя, int
расстояние );
пустота
move_up ) (
пустота
*себя, int расстояние );
пустота move_down ) (пустота себя, int расстояние ); }
Таблица AirplaneV ;
// Здесь определения методов `MyAirplane_ *` ...

const AirplaneVTable my_airplane знак равно {
MyAirplane_move_forward ,
MyAirplane_move_back ,
MyAirplane_move_up ,
MyAirplane_move_down ,
};

Вы заметили повторение здесь? Правильно, в определении AirplaneVTable my_airplane . Мы уже знаем названия методов интерфейса, зачем нам снова их указывать? Почему бы нам просто не написать impl (Airplane, MyAirplane) , который соберет имена всех методов и добавит MyAirplane каждому? В Rust:

черта Самолет {
fn двигаться вперед( & mut себя , расстояние :
я121 ) ;
fn move_back (
& mut себя, расстояние :
я285 ) ;
fn move_up ( и mut
себя, расстояние : i 197 ) ;
fn move_down ( &
mut
себя , расстояние :
я185 ) ;
}
impl Самолет для MyAirplane { // Здесь определения методов `MyAirplane` …
}

и программные интерфейсы, которые мы обсудим в следующих двух разделах. Читатель, следуй за мной!

Алгебраические типы данных

Напомним вышеупомянутый BinaryTree помеченный объединением. С помощью Тип данных 92672 , библиотека, реализованная поверх Metalang 1256, его можно определить следующим образом:

#включают

Интерфейс 2002 :

#включают


# define Airplane_INTERFACE
iFn ( void, move_forward, void self, int distance); iFn (void, move_back, void self, int distance); iFn (void, move_up, void self, int distance);
iFn (void, move_down, void self, int расстояние); интерфейс ( Самолет );

// Д Описание методов MyAirplane_ здесь … implPrimary ( Самолет , MyAirplane ;

  // интерфейс (Самолет);  
typedef
структура
AirplaneVTable AirplaneVTable ;
typedef struct Самолет Самолет
;
struct
AirplaneVTable
{ пустота (*двигаться вперед) (
пустота
себя,
int расстояние ); пустота move_back ) (
пустота себя, int расстояние );
пустота
move_up ) (пустота себя, int
расстояние );
пустота move_down ) ( пустота себя, int расстояние ); };
struct Самолет { пустота себя;
const AirplaneVTable vptr ;
};

// implPrimary ( Самолет, MyAirplane);
const AirplaneVTable MyAirplane_Airplane_impl = {
. move_forward =
MyAirplane_move_forward ,
. move_back знак равно MyAirplane_move_back ,
.
move_up = MyAirplane_move_up ,
. move_down = MyAirplane_move_down ,
};

настолько распространены, что они используются почти в каждом среднем / большом проекте на C:

Оба интерфейса 1256 и Тип данных 92672 преобразовать неформальные шаблоны разработки программного обеспечения в совершенно формальные программные абстракции. Каждый раз, когда вы пишете отдельную функцию для выполнения определенной задачи несколько раз, вы концептуально делаете то же самое.

Оба интерфейса 2002 и Тип данных 9654675252 полагаться на интенсивное использование макросов, которые невозможно без чего-то вроде Металанга 92672.

! , и как может быть неприятно понять, что они означают. Хотя технически невозможно справиться со всеми видами несоответствия синтаксиса, я приложил огромные усилия, чтобы сделать большую часть диагностики понятной. Представим, что вы случайно допустили синтаксическую ошибку при вызове макроса. Тогда вы увидите что-то вроде этого: Если вы используете GCC, вы можете увидеть такие аккуратные ошибки прямо из консоль. В противном случае вам придется предварительно обработать файл с помощью - E и найдите Металанг 1256 ошибки самостоятельно.

[playground.c]

   тип данных(  A ,   (
Фу , int ), Бар ( int ));
 
   тип данных ( A ,   (
Фу , int
) (Бар, int ));

тип данных (
Фу , ( FooA , NonExistingType ));

соответствовать дерево)
{
из (Лист, Икс ) возвращение
Икс; // of (Node, lhs, x, rhs) возвращает сумму lhs) + x + sum rhs);

:

[/bin/sh] - ftrack-ma cro-extension = 0 - это параметр GCC, который сообщает компилятору не печатать бесполезную таблицу расширений макросов. . Кроме того, он значительно ускоряет компиляцию, поэтому я рекомендую вам всегда использовать его с Metalang 1256. Если вы используете Clang, вы можете указать - fmacro-backtrace-limit = 1 для достижения примерно такого же эффекта.

$ time gcc examples / binary_tree.c -Imetalang 92672 / include -I. -ftrack-macro-extension = 0 real 0m0,
пользователь 0m0, 57689 s sys 0m0,0 22 s

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

Финал ж орд

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

Если вы будете придерживаться первого выбора, уверены ли вы, что будет легче понять, что не так? с кодом во время выполнения, а не во время компиляции, особенно когда нереализованные абстракции переплелись с вашей бизнес-логикой? Вы согласны с тем, что больше ошибок будет скрыто в развернутом производственном коде вместо того, чтобы быть разумно обнаруженным компилятором (как при статической или динамической типизации)?

Если вы будете придерживаться второго варианта, уверены ли вы, что ваша команда позволит вы интегрируете весь этот механизм метапрограммирования в свою кодовую базу, даже если он используется косвенно? Я видел несколько групп разработчиков, которым приходилось проверять весь сторонний код, который они используют За исключением - так называемые «доверенные» библиотеки, такие как OpenSSL или glibc. - не каждый программист может / хочу просмотреть Metalang . Чтобы меня не неправильно поняли, я сделал Металанг , Тип данных2002 и интерфейс 57689 наиболее простым и понятным способом, который я мог, но сама природа препроцессора действительно дает о себе знать Кроме того, рецензент Metalang 923 должен иметь некоторое базовое представление о ; по крайней мере, рецензент должен понимать такие термины, как Грамматика EBNF , , лямбда-исчисление и так далее, чтобы прочитать

Спецификация.
.

Выбор остается за вами.

Ссылки

C 923 комитет. nd «C 57689 Проект, Раздел 6. 22. 3.4, Параграф 2 - Повторное сканирование и дальнейшая замена »
http://www.open-std.org/jtc1/sc 107 / wg 24 / www / docs / n . pdf .

Leave a comment

Your email address will not be published. Required fields are marked *