_Generic макрос

Теги: _Generic, макрос, перегрузка функции в си



Дженерики

В Си стандарта С11 появились дженерик макросы, которые позволяют создавать функции общего назначения. Дженерик макрос позволяет выбрать функцию в зависимости от типа аргументов, переданных в функцию. Это, в некотором роде, сродни перегрузке методов по типу аргумента во многих языках, однако, это не динамическая диспетчеризация – определение типа аргумента происходит только на этапе компиляции.

Так как это нововведение Си 11, то для работы потребуется новая версия компилятора, которая поддерживает стандарт. Для примера используется gcc версии 5.3.0 с флагом -std=c11. На Windows без проблем ставится пакет MinGW, в состав которого входят все необходимые утилиты.

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

int foo_int(int a) {
    return a + 1;
}
float foo_float(float a) {
    return a + 1.0;
}
char foo_char(char a) {
    return a + 1;
}

Далее сам макрос

#define foo(X)          \
    _Generic((X),       \
    int: foo_int,       \
    char: foo_char,     \
    float: foo_float,   \
    default: foo_int    \
)(X)

Эта система похожа на оператор switch. Вместо foo будет подставлена одна из функций, а далее (X) – вызов с этим аргументом. Дефолтное значение – когда тип определить нельзя. Если тип не определён и нет дефолтного значения, или оно не может быть использовано функцией по умолчанию, то это ошибка. Вот программа

#include <stdio.h>
 
int foo_int(int a) {
    return a + 1;
}
float foo_float(float a) {
    return a + 1.0;
}
char foo_char(char a) {
    return a + 1;
}
 
#define foo(X)          \
    _Generic((X),       \
    int: foo_int,       \
    char: foo_char,     \
    float: foo_float,   \
    default: foo_int    \
)(X)
 
int main() {
    float a = foo(5.0f);
    int b = foo(5);
    char c = foo('a');
 
    printf("%.3f\n", a);
    printf("%d\n", b);
    printf("%c\n", c);
     
    return 0;
}

Программа выведет

6.000
6
b

Для функции с двумя аргументами немного сложнее, надо описать все комбинации. В нашем случае функция может принимать аргументы типа float и int, складывать их и возвращать целое

#include <stdio.h>
 
int baz_int_int(int a, int b) {
    return a + b;
}
 
int baz_int_float(int a, float b) {
    return a + (int) b;
}
 
int baz_float_int(float a, int b) {
    return (int) a + b;
}
 
int baz_float_float(float a, float b) {
    return (int) a + (int) b;
}
 
#define baz_int(X, Y) _Generic((Y),\
    int:   baz_int_int,\
    float: baz_int_float\
)
 
#define baz_float(X, Y) _Generic((Y),\
    int:   baz_float_int,\
    float: baz_float_float\
)
 
#define bar(X, Y) _Generic((X),\
    int:   baz_int(X, Y),\
    float: baz_float(X, Y)\
)(X, Y)
 
int main() { 
    int d = bar(1, 2);
        int e = bar(1.0f, 2.0f);

    printf("%d\n", d);
        printf("%d\n", e);
        
    return 0;
}

Конечно, можно вместо трёх макросов объединить всё в один

#include <stdio.h>
 
int baz_int_int(int a, int b) {
    return a + b;
}
 
int baz_int_float(int a, float b) {
    return a + (int) b;
}
 
int baz_float_int(float a, int b) {
    return (int) a + b;
}
 
int baz_float_float(float a, float b) {
    return (int) a + (int) b;
}
 
#define bar(X, Y) _Generic((X),\
    int:   _Generic((Y),\
                int:   baz_int_int,\
                float: baz_int_float\
        ),\
    float: _Generic((Y),\
                int:   baz_float_int,\
                float: baz_float_float\
        )\
)(X, Y)
 
int main() { 
    int d = bar(1, 2);
        int e = bar(1.0f, 2.0f);

    printf("%d\n", d);
        printf("%d\n", e);
        
    return 0;
}

Вот другой пример: макрос, который выводит имя типа, переданного в него

#include <stdio.h>
#include <stdint.h>

#define typeof_name(X) _Generic ((X),\
        int:                    "signed integer",\
        unsigned:               "unsigned integer",\
        char:                   "signed char",\
        unsigned char:  "unsigned char",\
        float:                  "float",\
        double:                 "double",\
        char*:                  "modifiable string",\
        const char*:    "const string",\
        default:                "unknown"\
)

#define print_type(X) printf("%s\n", typeof_name(X))
 
int main() {
        print_type('a');
        print_type((int64_t) 32);
        print_type("string");
     
    return 0;
}

Заметьте важные особенности – без явного указания ‘a’ будет рассматриваться как int, а строка как модифицируемая, хотя попытка её изменить приведёт к ошибке. Для точного определениятипа можно его явно привести

print_type((unsigned char)'a');
print_type((int64_t) 32);
print_type((const char*)"string");

Также тип может быть аргументом макроса

#include <stdio.h>

#define is_compatible(x, T) _Generic((x), T:1, default: 0)
 
int main() {
        int a = is_compatible(3, int);
        int b = is_compatible(3, float);
        printf("a = %d and b = %d", a, b);
     
    return 0;
}
Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Реализация пространств имён в си