Введение

Язык C по сравнению с другими языками программирования, такими как C++, Java и Python, может показаться менее удобным с точки зрения синтаксических конструкций и возможностей. Тем не менее, время от времени можно обнаружить приемы, которые вносят некоторое удобство в разработку на C. В этой статье я хочу поделиться одной из таких техник, которую я почерпнул из книги Бена Клеменса 21st Century C (эту книгу я очень рекомендую к прочтению даже разработчикам, использующим C в своей работе уже много лет, предлагает свежий взгляд на язык). Этот метод позволяет использовать аргументы по умолчанию и именованные аргументы в языке C. Однако одно дело прочитать об интересной функции, а другое — применить ее на практике. Недавно у меня была возможность применить советы из этой книги в реальном сценарии, что и натолкнуло меня на мысль написать эту статью.

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

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

ShowScreenBase(ScreenType_e type,
              char* text,
              ActionHandler action,
              PictureId_e picture,
              ScreenConfig_t config,
              BacklightColor_e backlight);

Разберем каждый аргумент:

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

Чтобы вызвать эту функцию, мы должны использовать следующий код:

ShowScreenBase(WarningScreen,
              "Attention! Bla-bla-bla",
              AttentionAction,
              Attention_PictureId,
              (ScreenConfig_t){.align=Top},
              BacklightColor_yellow);

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

ShowScreenBase(type=WarningScreen,
              text="Attention! Bla-bla-bla",
              action=AttentionAction,
              backlight=BacklightColor_yellow);

Такой вызов функции становится намного читабельнее и понятнее. И самое интересное, что это возможно.

Для этого нам понадобятся три вещи: вариативный макрос, составной литерал и переопределение инициализации структуры.

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

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

Синтаксис составного литерала выглядит следующим образом:

(type){ initializer-list }

Здесь type представляет тип данных, а список-инициализаторов — это список инициализаторов, используемых для инициализации объекта.

Вот несколько примеров составных литералов:

  • Создание и инициализация временного массива:
int* arr = (int[]){1, 2, 3, 4, 5};
  • Создание и инициализация временной структуры:
typedef struct 
{
 int x;
 int y;
} Point;

Point p = (Point){.x = 10, .y = 20};
  • Создание и инициализация временного союза:
typedef union 
{
 int intValue;
 float floatValue;
} Data;

Data d = (Data){.floatValue = 3.14f};

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

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

Вариативные макросы

Макросы с переменным числом аргументов в языке C предоставляют возможность определять макросы, которые могут принимать различное количество аргументов во время вызова. Они представляют собой мощный инструмент для работы с переменным числом аргументов и упрощения написания гибкого кода.

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

Рассмотрим пример вариативного макроса:

#include <stdio.h>

#define PRINT_ARGS(...) \
    printf(__VA_ARGS__)

int main() {
    PRINT_ARGS("Hello, ", "World!\n");
    PRINT_ARGS("The sum of %d and %d is %d.\n", 10, 5, 15);
    
    return 0;
}

В этом примере макрос PRINT_ARGS принимает переменное количество аргументов и передает их функции printf с помощью __VA_ARGS__. Это позволяет нам передавать разное количество аргументов при каждом вызове PRINT_ARGS.

Важно отметить, что __VA_ARGS__ следует размещать там, где аргументы должны быть заменены внутри вариативного макроса.

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

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

Инициализация переопределения структуры

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

Это означает, что если у нас есть структура типа:

struct ScreenArg 
{
 ScreenType_e type;
 char* text;
 ActionHandler action;
 PictureId_e picture;
 ScreenConfig_t config;
 BacklightColor_e backlight;
};

Мы можем использовать Override Initialization для инициализации этой структуры следующим образом:

struct ScreenArg arg =
{
 .type=Screen_Default,\
 .text=EMPTY_TEXT,\
 .action=NULL,\
 .picture=EMPTY_PICTURE,\
 .config=&cfg_default,\
 .backlight=NONE,\
 .type=WarningScreen,\
 .text="Attention! Bla-bla-bla",\
 .action=AttentionAction
};

В этом примере мы видим, что некоторые поля структуры ScreenArg явно дублируются в списке инициализации. Эти повторяющиеся поля: тип, текст и действие будут инициализированы последними указанными значениями: WarningScreen, ” Внимание! Бла-бла-бла» и AttentionActionn соответственно.

Пример макроса с аргументами по умолчанию

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

Во-первых, давайте создадим отдельную структуру ScreenArg, которая будет содержать аргументы функции ShowScreenBase:

typedef struct 
{
 ScreenType_e type;
 char* text;
 ActionHandler action;
 PictureId_e picture;
 ScreenConfig_t config;
 BacklightColor_e backlight;
} ScreenArg;

Затем мы определяем макрос ShowScreen, используя Compound Literal для определения значений по умолчанию и Override Initialization для переопределения этих значений:

#define ShowScreen(...) (ShowScreenBase((ScreenArg){ \
 .type=Screen_Default, \
 .text=EMPTY_TEXT, \
 .action=NULL, \
 .picture=EMPTY_PICTURE, \
 .config=cfg_default, \
 .backlight=NONE, \
 __VA_ARGS__ }))

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

Приведем примеры вызова макроса ShowScreen:

  • Вызов макроса без аргументов:
ShowScreen();

В этом случае будут использоваться значения по умолчанию, и он расширится до следующего вызова:

ShowScreen((ScreenArg){
  .type=Screen_Default,
  .text=EMPTY_TEXT,
  .action=NULL,
  .picture=EMPTY_PICTURE,
  .config=cfg_default,
  .backlight=NONE});
  • Вызов макроса с полным набором аргументов:
ShowScreen(
 .type=WarningScreen,
 .text="Attention!Bla-bla-bla",
 .action=AttentionAction,
 .picture=WARN_PICTURE,
 .config=cfg_default,
 .backlight=BacklightColor_yellow);

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

  • Вызов макроса с переопределением только определенных аргументов:
ShowScreen(
  .type=WarningScreen,
  .text="Attention! Bla-bla-bla",
  .action=AttentionAction);

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

ShowScreen((ScreenArg){
  .type=WarningScreen,
  .text="Attention! Bla-bla-bla",
  .action=AttentionAction,
  .picture=EMPTY_PICTURE,
  .config=cfg_default,
  .backlight=NONE});

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

Возможные проблемы

При использовании переопределения инициализации могут возникнуть проблемы с некоторыми компиляторами. Некоторые компиляторы могут испытывать трудности с интерпретацией этой функции и выдавать предупреждения. Если ваша сборка настроена с флагами компиляции -Wall и -Werror, эти предупреждения могут стать проблемой, поскольку компилятор рассматривает их как ошибки.

Одно из возможных предупреждений, связанных с инициализацией переопределения, может быть связано с потенциальным переназначением значений при инициализации структуры. Например, на это может указывать предупреждение -override-init.

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