Директива #ifdef и управление вариантами компиляции

Директива #ifdef и условная компиляция

Последнее обновление: 19.09.2023

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

Кроме того, возможно, мы захотим, чтобы при одних конфигурационных настройках применялись одни функции, при других же настройках - другие функции. Это ставит нас перед проблемой вариативности программы - программа определяет различные варианты, которые будут компилироваться при определенном наборе настроек. Например, если программа компилируется под Linux, вызываем и используем одни функции, если же под Windows - используем другие функции. В общем случае для управления вариантами компиляции применяется препроцессорная директива #ifdef.

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

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

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef __unix__
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <unistd.h>
#elif defined _WIN32
  #include <windows.h>
#endif

int main(void)
{
  char dirname[60];         // пусть к папке
  char filename[60];        // пусть к файлу
  char* text = "Hello METANIT.COM";      // некоторый текст для сохранения
  #ifdef __unix__           // если Linux
    #ifdef HOME_DIR         // если сохраняем в домашнем каталоге пользователя
      sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/");
      sprintf(filename, "%s%s", dirname, "newfile");
    #elif defined CURRENT_DIR       // если сохраняем текущем каталоге
      strcpy(dirname, "newdir");
      strcpy(filename, "newdir/newfile");
    #endif
    mkdir(dirname,S_IRWXU);
  #elif defined _WIN32      // если Windows
    #ifdef HOME_DIR         // если сохраняем в домашнем каталоге пользователя
      sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\");
      sprintf(filename, "%s%s", dirname, "newfile");
    #elif defined CURRENT_DIR              // если сохраняем текущем каталоге
      strcpy(dirname, "newdir");
      strcpy(filename, "newdir\newfile");
    #endif
    CreateDirectory (dirname, NULL);
  #endif
  // запись файла
  FILE* f = fopen(filename, "w+");
  fwrite(text, 1, strlen(text), f);
  fclose(f);

  return 0;
}

Здесь мы проверяем установку двух констант. Если установлена константа __unix__, то мы имеем дело с Linux, если же установлена константа _WIN32, то программа компилируется для Windows. В зависимости от установки констант, подключаем те или иные заголовочные файлы:

#ifdef __unix__
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <unistd.h>
#elif defined _WIN32
  #include <windows.h>
#endif

Затем в зависимости от системы в функции main выполняем создание каталога и запись в него файла:

#ifdef __unix__           // если Linux
    #ifdef HOME_DIR         // если сохраняем в домашнем каталоге пользователя
      sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/");
      sprintf(filename, "%s%s", dirname, "newfile");
    #elif defined CURRENT_DIR       // если сохраняем текущем каталоге
      strcpy(dirname, "newdir");
      strcpy(filename, "newdir/newfile");
    #endif
    mkdir(dirname,S_IRWXU);
  #elif defined _WIN32      // если Windows
    #ifdef HOME_DIR         // если сохраняем в домашнем каталоге пользователя
      sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\");
      sprintf(filename, "%s%s", dirname, "newfile");
    #elif defined CURRENT_DIR              // если сохраняем текущем каталоге
      strcpy(dirname, "newdir");
      strcpy(filename, "newdir\newfile");
    #endif
    CreateDirectory (dirname, NULL);
  #endif

Если установлена константа HOME_DIR каталог создается в домашней папке пользователя, а если установлена константа CURRENT_DIR - в текущей папке программы.

То есть фактически здесь определены две разные реализации для конкретной операционной системы, помещенные в один файл. Однако в таком коде трудно ориентироваться. Платформо-зависимый и платформо-независимый код смешены в одной функции. А что, если добавить еще одно выражение #ifdef для еще одной операционной системы? Поддерживаемость и читабельность кода будет стремиться к нулю. Посмотрим, как исправить эту ситуацию.

Выделение вариантов кода в отдельную функцию

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

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef __unix__
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <unistd.h>
#elif defined _WIN32
  #include <windows.h>
#endif

void getDirectoryName(char* dirname)
{
  #ifdef __unix__
    #ifdef HOME_DIR
      sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/");
    #elif defined CURRENT_DIR
      strcpy(dirname, "newdir/");
    #endif
  #elif defined _WIN32
    #ifdef HOME_DIR
      sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\");
    #elif defined CURRENT_DIR
      strcpy(dirname, "newdir\");
    #endif
  #endif
}

void createNewDirectory(char* dirname)
{
  #ifdef __unix__
    mkdir(dirname,S_IRWXU);
  #elif defined _WIN32
    CreateDirectory (dirname, NULL);
  #endif
}

int main(void)
{
  char dirname[60];
  char filename[60];
  char* text = "Hello METANIT.COM";
  getDirectoryName(dirname);
  createNewDirectory(dirname);
  sprintf(filename, "%s%s", dirname, "newfile");
  FILE* f = fopen(filename, "w+");
  fwrite(text, 1, strlen(text), f);
  fclose(f);
  return 0;
}

Теперь платформо-зависимый код выделен в две функции - getDirectoryName (для получения пути к папке) и createNewDirectory (для создания новой папки). В таком коде гораздо проще ориентироваться.

Тем не менее здесь сохраняется другая проблема - дублирование кода - мы дублируем проверку констант __unix__ и _WIN32, чтобы определить текущую систему. И также дублируем проверку констант HOME_DIR и CURRENT_DIR, чтобы определить каталог. А если мы захотим добавить поддержку еще одной платформы, то нам придется менять код в нескольких местах.

Определение примитивов для работы с вариантами

Частичное решение проблемы дублирования кода проверки условий компиляции в общем случае заключается в вынесении работы с отдельными вариантами в отдельные примитивы - функции:

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef __unix__
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <unistd.h>
#elif defined _WIN32
  #include <windows.h>
#endif


void getHomeDirectory(char* dirname)
{
  #ifdef __unix__
    sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/");
  #elif defined _WIN32
    sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\");
  #endif
}

void getCurrentDirectory(char* dirname)
{
  #ifdef __unix__
    strcpy(dirname, "newdir/");
  #elif defined _WIN32
    strcpy(dirname, "newdir\");
  #endif
}

void getDirectoryName(char* dirname)
{
  #ifdef HOME_DIR
    getHomeDirectory(dirname);
  #elif defined CURRENT_DIR
    getCurrentDirectory(dirname);
  #endif
}

void createNewDirectory(char* dirname)
{
  #ifdef __unix__
    mkdir(dirname,S_IRWXU);
  #elif defined _WIN32
    CreateDirectory (dirname, NULL);
  #endif
}

int main(void)
{
  char dirname[60];
  char filename[60];
  char* text = "Hello METANIT.COM";
  getDirectoryName(dirname);
  createNewDirectory(dirname);
  sprintf(filename, "%s%s", dirname, "newfile");
  FILE* f = fopen(filename, "w+");
  fwrite(text, 1, strlen(text), f);
  fclose(f);
  return 0;
}

Теперь получание пути к папке разбита на две функции - два варианта - для домашней папки пользователя и для текущей папки:

void getHomeDirectory(char* dirname)
{
  #ifdef __unix__
    sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/");
  #elif defined _WIN32
    sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\");
  #endif
}

void getCurrentDirectory(char* dirname)
{
  #ifdef __unix__
    strcpy(dirname, "newdir/");
  #elif defined _WIN32
    strcpy(dirname, "newdir\");
  #endif
}

Затем в функции getDirectoryName в зависимости от установки констант вызываем ту или иную функцию:

void getDirectoryName(char* dirname)
{
  #ifdef HOME_DIR
    getHomeDirectory(dirname);
  #elif defined CURRENT_DIR
    getCurrentDirectory(dirname);
  #endif
}

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

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850