Допустим, у нас есть следующие параметры, описывающие человека: имя (строка) и возраст (целое без знака). Я хочу написать универсальную API-функцию сеттера, которую кто-то может вызвать, чтобы установить имя или возраст конкретного человека. Причина этого будет объяснена ниже.
Что я сделал, так это определил тип перечисления имен параметров человека:
typedef enum person_param_name
{
NAME,
AGE,
} person_param_name_t;
А также тип объединения для значений параметров человека:
typedef union person_param_val
{
char* name;
unsigned int age;
} person_param_val_t;
Теперь функция может выглядеть так:
int set_person_param(person_param_name_t param_name, person_param_val_t param_val)
{
int ret = 0;
switch (param_name)
{
case NAME:
g_person_name = param_val.name;
break;
case AGE:
g_person_age = param_val.age;
break;
default:
ret = -1;
break;
}
return ret;
}
Проблема с этим подходом заключается в том, что нельзя просто вызвать функцию установки следующим образом (компилятор выдает предупреждение):
set_person_param(NAME, "Alex");
set_person_param(AGE, 5);
Но они должны явно привести значение параметра к типу person_param_val_t, например:
set_person_param(NAME, (person_param_val_t)"Alex");
set_person_param(AGE, (person_param_val_t )5);
Причина, по которой мне нужна универсальная функция установки, заключается в том, что в реальной программе у меня намного больше параметров (около 100), и мне нужно было бы написать много (очень похожих) функций установки, которые заняли бы намного больше строк кода.
Есть ли лучший подход к этому?
Звучит как работа для _Generic
@Barmar _Generic устроит беспорядок. На данный момент я не уверен, знает ли ОП, что такое союз.
@0___________ Они назначают разные переменные, поэтому ничего не уничтожается.
Тогда я не могу понять, почему два разных союза вместо одной структуры. Это похоже на проблему XY для меня
Вы можете использовать составные литералы: set_person_param(NAME, (person_param_val_t){ .name = "Alex"}); set_person_param(AGE, (person_param_val_t ){ .age = 5} );
«Мне нужно было бы написать много (очень похожих) функций установки, которые заняли бы намного больше строк кода», 4 строки для функции настройки, а не 3 строки для добавления случая не намного больше, и это более понятно .
@dbush ты понимаешь, почему объединение и странные глобальные переменные
Что касается кода вашего примера, то ни (person_param_val_t)"Alex", ни (person_param_val_t )5 не будут работать. Стандарт C определяет приведение только к void или скалярным типам, а не к объединениям (или структурам или массивам).
Звучит как работа для простого макроса, никаких союзов или подобных причудливых вещей
У людей нет ни имени, ни возраста. У них есть и имя, и возраст. И вполне возможно иметь возраст и имя одновременно. Таким образом, было бы более разумно хранить и возраст, и имя в структуре, а затем найти способ пометить любой элемент недействительным. Например, оба члена могут быть указателями на данные, установленные в NULL, если они не инициализированы.
@EricPostpischil Приведение к объединениям не является частью стандарта C, но расширение GNU C позволяет это делать.
@A6SE: если вы хотите разрешить расширения определенного компилятора, это должно быть указано в вопросе и добавлено как тег. Значение тега C по умолчанию — стандарт: «Этот тег следует использовать с общими вопросами, касающимися языка C, как определено в стандарте ISO 9899 (последняя версия, 9899:2018, если не указано иное…)».
Я мог видеть:
typedef enum person_param_name {
NAME,
AGE,
} person_param_name_t;
typedef union person_param_val
{
char* name;
unsigned int age;
} person_param_val_t;
person_param_val_t person_param_val_init_charp(char *name) {
return (person_param_val_t){.name=name};
}
person_param_val_t person_param_val_init_u(unsigned age) {
return (person_param_val_t){.age=age};
}
#define MAKE_PERSON_PARAM_VAL(x) _Generic((x) \
, unsigned: person_param_val_init_u \
, char *:person_param_val_init_charp \
)(x)
int set_person_param(person_param_name_t param_name, person_param_val_t param_val);
#define set_person_param(a, b) \
set_person_param(a, MAKE_PERSON_PARAM_VAL(b))
int main() {
set_person_param(NAME, "Alex");
set_person_param(AGE, 5u);
}
С GCC с расширением вы получите только:
#define set_person_param(a, b) \
set_person_param(a, (person_param_val_t)(b))
Но я бы не стал писать такой код. Это C. В C вы бы написали все это явно. Не вижу смысла в person_param_name. Вам все равно придется явно перечислять все типы внутри set_person_param. Я бы просто написал set_person_param_age(unsigned age) и set_person_param_name(char *name) явно. Если нет, я бы подумал о переосмыслении всего подхода, поскольку, скорее всего, вы хотите реализовать виртуальную функцию. Я бы посоветовал настоятельно не писать интерфейс с бесконечным количеством случаев в перечислениях, потому что вы можете получить это. Вместо этого создайте объекты с указателем на интерфейс, хранящийся в виртуальной таблице.
Где волшебные g_... переменные, как в примере OP
Вам не нужна никакая магия. этого макроса достаточно
#define set_person_param(param, val) g_person_##param.param = (val)
И эта примерная функция:
int foo(void)
{
person_param_val_t g_person_name, g_person_age;
set_person_param(name, "Alex");
set_person_param(age, 5);
}
Будет предварительно обработан для:
int foo(void)
{
person_param_val_t g_person_name, g_person_age;
g_person_name.name = "Alex";
g_person_age.age = 5;
}
Насколько я понимаю, это было что-то, что вы хотели заархивировать. Ваш тип перечисления не нужен.
Если вы хотите, чтобы val был того же типа союза, то:
#define set_person_param(param, val) g_person_##param = (val)
Пример:
set_person_param(name, (person_param_val_t){.name = "Alex"});
Если вы измените объединение так, чтобы имена полей были идентичны константам перечисления:
typedef union person_param_val
{
char* NAME;
unsigned int AGE;
} person_param_val_t;
Затем вы можете создать макрос, который будет передавать правильно инициализированный составной литерал:
#define set_person_param_ext(k,v) \
set_person_param(k, (person_param_val_t){.k=v})
Тогда это:
set_person_param_ext(NAME, "Alex");
set_person_param_ext(AGE, 5);
Будет расширяться до этого:
set_person_param(NAME, (person_param_val_t){.NAME = "Alex"});
set_person_param(AGE, (person_param_val_t){.AGE=5});
Знаете ли вы, что установка age уничтожит name и наоборот?