Обобщения и макрос _Generic

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

Обобщения или generics представляют универсальные функции или типы, которые не зависят от используемых в них типов данных (еще называется параметрическим полиморфизмом). Нативно язык Си не поддерживает обобщения. Однако благодаря макросам можно до некоторой степени имитировать подобную функциональность. Кроме того, начиная со стандарта C11 был добавлен специальный макрос _Generics, который мозволяет упростить определение обобщений.

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

#include <stdio.h>

#define define_sum(T) T sum_##T(T a, T b) {\
 return a + b; \
}

#define sum(T) sum_##T

define_sum(int)
define_sum(float)

int main(void)
{
    printf("float: %f\n", sum(float)(3.0, 2.0));
    printf("int: %d\n", sum(int)(3, 2));
    return 0;
}

Здесь определено два макроса. Первый макрос - define_sum(T) предназначен для определения функции сложения:

#define define_sum(T) T sum_##T(T a, T b) {\
 return a + b; \
}

Через параметр T будет передаваться тип данных. С помощью оператора ## название типа будет присоединяться к названию функции sum. Например, если в макрос передается тип "int", то будет определяться функция "sum_int". Для переноса кода макроса на последующие строки применяется слеш \

Второй макрос устанавливает вызов определенной функции

#define sum(T) sum_##T

Например, первый вызов макроса define_sum

define_sum(int)

разворачивается в следующий код:

int sum_int(int a, int b) {
 return a + b;
}

При вызове второго макроса - "sum(T)" будет происходить вызов функции:

printf("int: %d\n", sum(int)(3, 2));

то есть фактически мы получим код:

printf("int: %d\n", sum_int(3, 2));

Аналогично можно создавать псевдоуниверсальные структуры:

#include <stdio.h>

#define DEFINE_PERSON(T, N) struct Person_##N{\
    T id;\
    char name[20];\
};

DEFINE_PERSON(int, int)
DEFINE_PERSON(char*, char)

#define person(N) struct Person_##N


int main(void)
{
    person(char) tom = {.id="regjh12345", .name="Tom"};
    person(int) bob = {.id=12344, .name="Bob"};

    printf("id: %s  name: %s\n", tom.id, tom.name);
    printf("id: %d  name: %s\n", bob.id, bob.name);
    return 0;
}

В данном случае макрос DEFINE_PERSON разворачивается в структуру, где тип поля id зависит параметра макроса, через который передается тип данных. Первый параметр макроса указывает на тип поля id, а второй параметр служит для установки уникального имени структуры. Например, при вызове макроса DEFINE_PERSON(char*, char) мы получим определение следующей структуры:

struct Person_char{
    chr* id;
    char name[20];
};

Макрос _Generic

В стандарт C11 был добавлен макрос _Generic, который позволяет в ряде случаев уйти от жесткой привязки к типам данных в выражениях. Этот макрос при компиляции на основании типа данных выбирает нужную реализацию выражения. Он имеет следующее определение:

_Generic (контролирующее_выражение, список_ассоциаций)

Первый параметр или контролирующее_выражение представляет выражение, тип которого будет сравниваться с типами из второго параметра - списка ассоциаций

Второй параметр или список_ассоциаций представляет список разделенных запятыми сопоставлений типов и выражений в следующем виде:

тип1 : выражение1,
тип2 : выражение2,
default : выражение по умолчанию

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

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

#include <stdio.h>

// определение обобщенной функции sum
#define sum(a, b) _Generic((a), \
	int : sum_int, \
	float : sum_float, \
	default : sum_double \
)(a, b)

// отдельные реализации функции
void sum_int(int a, int b)
{
	printf("Sum of ints: %d\n", a + b);
}
void sum_float(float a, float b)
{
 	printf("Sum of floats: %f\n", a + b);
}
void sum_double(double a, double b)
{
 	printf("Sum of numbers: %lf\n", a + b);
}
int main(void)
{
	int int_num1 = 4;
	int int_num2 = 5;

	float float_num1 = 5.6f;
	float float_num2 = 6.5f;

	double double_num1 = 2.3l;
	double double_num2 = 3.7l;

	sum(int_num1, int_num2);
	sum(float_num1, float_num2);
	sum(double_num1, double_num2);
	
	return 0;
}

Вначале определяем обобщенную функцию sum(), которая не будет привязана конкретным типам:

#define sum(a, b) _Generic((a), \
	int : sum_int, \
	float : sum_float, \
	default : sum_double \
)(a, b)

Здесь sum(a, b) указывает, что функция будет называться "sum" и будет принимать два параметра.

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

Затем определяется список ассоциаций - int : sum_int, float : sum_float, default : sum_double. То есть если параметр a представляет тип int, то в качестве реализации функции sum() в реальности будет использоваться функция sum_int(). Если тип параметра float, то выбирается функция sum_float. Во всех остальных случаях выбирается функция sum_double().

Далее в скобках указываются параметры для вызова выбранной функции: (a, b)

После определения обобщенной функции sum() определяются конкретные реализации, которые будут использоваться в зависимости от типа данных:

void sum_int(int a, int b)
{
	printf("Sum of ints: %d\n", a + b);
}
void sum_float(float a, float b)
{
 	printf("Sum of floats: %f\n", a + b);
}
void sum_double(double a, double b)
{
 	printf("Sum of numbers: %lf\n", a + b);
}

И в функции main вызываем функцию sum(), передавая ей значения определенного типа. А на основании типа первого параметра будет выбираться конкретная реализация:

sum(int_num1, int_num2);
sum(float_num1, float_num2);
sum(double_num1, double_num2);

То есть мы вызываем функцию sum(), но за кадром в реальности вызывается функция, предназначенная для определенных типов.

Консольный вывод:

Sum of ints: 9
Sum of floats: 12.100000
Sum of numbers: 6.000000
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850