Именованные аргументы функции в C

В некоторых языках существует возможность вызова функции с именованными параметрами. Такой способ позволяет указать аргумент для определённого параметра, связав его с именем параметра, а не с позицией. Это возможно, например, в C# или Python.

Рассмотрим «игрушечный» пример на Python с использованием именованных аргументов:

#вычислим объем параллелепипеда
#если значение стороны не указано, то считаем что оно равно единице
def volume(length=1, width=1, height=1): 
  return length * width * height; 
print(volume())                            # V = 1 
print(volume(length=2))                    # V = 2 
print(volume(length=2, width=3))           # V = 6 
print(volume(length=2, width=3, height=4)) # V = 24

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

Ниже я покажу, как можно сымитировать использование именованных аргументов в C.

Меньше слов — больше кода


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

#include <stdio.h> 

typedef struct { 
    int length, width, height; 
} params_s; 

int volume_f(params_s in) { 
    return in. length * in. width * in.height ; 
} 

int main() { 
    params_s p = {.length=8, .width=4, .height=2}; 
    /* Volume1 = 64 */ 
    printf("Volume1 = %i\n", volume_f(p));
    /* Volume2 = 0 */ 
    printf("Volume2 = %i\n", volume_f( (params_s){.width=4, .height=2}) ); 
    return 0; 
}

Стало немного лучше, но все равно осталась проблема с параметрами по умолчанию, если они отличны от нуля. Так в примере выше Volume2 = 0, т.к. поле length по умолчанию проинициализировалось нулем. Еще за именованные аргументы мы платим тем, что должны создавать структуру или помнить ее название, если делаем приведение типов. Да и делать постоянно приведение типов неудобно. Но на помощь приходят…

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


Макросы, которые принимают переменное число аргументов, появились в C99. Объявляются они также как и функция, которая принимает переменное число аргументов: нужно добавить многоточие в качестве последнего аргумента. Идентификатор __VA_ARGS__ заменяется аргументами, переданными в многоточии, включая запятые (точки с запятой) между ними. Сферический пример ниже.

#include <stdio.h> 
#define printArray(str, ...) {          \
    double d[] = {__VA_ARGS__, 0} ;     \
    puts(str);                          \
    for(int i = 0; d[i] !=0; i++)       \
        printf("%g ", d[i]);            \
    puts("");                           \
} 
#define DO(...){ __VA_ARGS__ } 
int main() { 
    printArray("cool array: ", 1, 2, 3, 4, 5); 
/* обратите внимание, что функции перечислены через точку с запятой */ 
    DO(puts("hello"); puts("world"); return 0); 
    return 0; 
}

После работы препроцессора макросы развернутся в такой код:

int main() { 
    { double d[] = {1, 2, 3, 4, 5, 0} ;  /*...*/}; 
   { puts("hello"); puts("world"); return 0;}; 
    return 0; 
} 

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

Итог


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

В итоге получился такой код:

#include <stdio.h> 

typedef struct { 
    int length, width, height; 
} params_s; 

int volume_f(params_s in) { 
    return in. length * in.width * in.height ; 
}
#define volume(...) \ 
	volume_f((params_s){.length=1, .width=1, .height=1 , __VA_ARGS__}) 

int main() { 
    printf("volume(): %i\n", volume()); 
    printf("volume(.length = 2): %i\n", volume(.length =2 )); 
    printf("volume(.length = 2, .width = 3): %i\n", 
        volume(.length = 2, .width = 3)); 
    printf("volume(.length = 2, .width = 3, .height =4): %i\n", 
        volume(.length =2, .width =3, .height =4)); 
}

Все примеры компилируется с флагом -std=c99 или -std=gnu99
Т.к. при вызове функции происходит переприсвоение значений полям структуры, то компиляторы выдают варнинг.

GCC выдаст:

warning: initialized field overwritten [-Woverride-init] 
clang:
warning: initializer overrides prior initialization of this subobject [-Winitializer-overrides].

Если надо его отключить, используем соответственно флаги: -Wno-initializer-overrides для clang или -Wno-override-init для gcc.

Подробней про вариативный макрос написано, например, в Википедии
Идея взята из книги Бена Клеменса
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 12

    +4
    Вот ещё попытка сделать С более гибким
    github.com/orangeduck/libCello
      +5
      Можно так:

      int volume(params_s in) { 
          return in. length * in.width * in.height ; 
      }
      #define volume(...) \ 
          volume((params_s){.length=1, .width=1, .height=1 , __VA_ARGS__})
      

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

      Идея интересная, главное не переборщить как в libCello. Иногда читая код, перегруженный макросами, появляется непреодолимое желание застрелить автора.
        0
        Разве передача именованных параметров является частью стандарта C? Про extension для компиляторов слышал, про возможности самого языка — нет.
          0
          Удалено.
            +1
            Да, в С99. Называется designated initializers.
              +2
              Тогда я очень недоволен MS и их «поддержкой» C99.
                0
                Надо отметить, что в стандарт С++ (не обычного C) эта фишка ещё не вошла, возможно дело в этом.
            0
              +2
              Сомнительные преимущества явно меркнут перед дополнительными плясками с бубном.
                +1
                С одной стороны здесь требуется создание дополнительного типа на каждую функцию.
                С другой стороны обычно в коде не так уж и много функций, которые имеют настолько длинный набор параметров, что надо делать именованные аргументы.
                С третьей стороны для этого небольшого количества функций можно и «руками» завести структуру, инициализировать её через designated inits и передавать в функцию, чтобы не вызывать разрыва мозга у читателя кода.

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

                FUNC (X=> X0, Y=> s1);

                Only users with full accounts can post comments. Log in, please.