Как создать функцию C с параметрами указателя void и условно привести их к другим типам во время выполнения? - PullRequest
0 голосов
/ 21 января 2020

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

void test_function(int use_type, void * value, void * array) {
    // Set types to the parameters based on 'use_type'
    if (use_type == 0) { // Int type
        int * valueT = (int *) value;
        int * arrayT = (int *) array;
    } else if (use_type == 1) { // Double type
        double * valueT = (double *) value;
        double * arrayT = (double *) array;
    }
    // Main code of the program, setting an array item, regardless of type
    arrayT[0] = *valueT;
}

С вышеуказанным кодом есть две проблемы: правильно набранные valueT и arrayT ограничены в условных блоках и не видны основной часть кода. Перемещение их объявлений из блоков не является жизнеспособным в данной структуре кода, так как тогда им потребуются разные имена для версий int и double, что подрывает всю идею того, чего я пытаюсь достичь. Другая проблема заключается в том, что valueT и arrayT являются локальными для функции. Что я действительно хочу, так это установить параметр array: array[0] = *value.

Кажется, что то, что я пытаюсь сделать, невозможно в C ... Есть ли способ, которым это может быть сделано?

РЕДАКТИРОВАТЬ:

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

Ответы [ 4 ]

4 голосов
/ 22 января 2020

Вы пытаетесь реализовать полиморфизм в C. На этом пути лежит безумие, не поддерживаемый код и новые языки программирования.

Вместо этого я настоятельно рекомендую рефакторинг вашего кода, чтобы использовать лучший метод работы со смешанными данными , union или struct или указатели или любое из решений здесь . Это будет меньше работать в долгосрочной перспективе и приведет к более быстрому и более обслуживаемому коду.

Или вы можете переключиться на C ++ и использовать шаблоны .

Или вы можете использовать чья-то реализация вроде GLib's GArray. Это система умных макросов и функций, обеспечивающих легкий доступ к любому типу данных в массиве. Он с открытым исходным кодом, поэтому вы можете изучить его реализацию , сочетание макросов и умных функций. Он имеет множество функций, таких как автоматическое изменение размера c и сборка мусора. И он очень зрелый и хорошо протестированный.

GArray запоминает свой тип, поэтому нет необходимости постоянно повторять его.

    GArray *ints = g_array_new(FALSE, FALSE, sizeof(int));
    GArray *doubles = g_array_new(FALSE, FALSE, sizeof(double));

    int val1 = 23;
    double val2 = 42.23;

    g_array_append_val(ints, val1);
    g_array_append_val(doubles, val2);

Базовый простой массив C может быть доступ к нему осуществляется как data поле GArray struct . Он набирается gchar *, поэтому его необходимо преобразовать.

    double *doubles_array = (double *)doubles->data;
    printf("%f", doubles_array[0]);

Если мы продолжим ваш путь, неопределенность в отношении типа заражает каждую функцию «generi c», и вы начинаете писать параллельно реализации в любом случае.

Например, давайте напишем функцию, которая добавляет два индекса вместе. Что-то, что должно быть простым.

Во-первых, давайте сделаем это условно.

int add_int(int *array, size_t idx1, size_t idx2) {
    return array[idx1] + array[idx2];
}

double add_double(double *array, size_t idx1, size_t idx2) {
    return array[idx1] + array[idx2];
}

int main() {
    int ints[] = {5, 10, 15, 20};

    int value = add_int(ints, 1, 2);

    printf("%d\n", value);
}

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

#define add(a, t, i1, i2) (add_ ## t(a, i1, i2))

int main() {
    int ints[] = {5, 10, 15, 20};

    int value = add(ints, int, 1, 2);

    printf("%d\n", value);
}

Макрос умный, но, вероятно, не стоит дополнительной сложности. Пока вы последовательны в именовании, программист может выбирать между формами _int и _double. Но он есть, если хотите.


Теперь давайте посмотрим на это с помощью функции "one".

// Using an enum gives us some type safety and code clarity.
enum Types { _int, _double };

void *add(void * array, enum Types type, size_t idx1, size_t idx2) {
    // Using an enum on a switch, with -Wswitch, will warn us if we miss a type.
    switch(type) {
        case _int : {
            int *sum = malloc(sizeof(int));
            *sum = (int *){array}[idx1] + (int *){array}[idx2];
            return sum;
        };
        case _double : {
            double *sum = malloc(sizeof(double));
            *sum = (double *){array}[idx1] + (double *){array}[idx2];
            return sum;
        };
    }; 
}

int main() {
    int ints[] = {5, 10, 15, 20};

    int value = *(int *)add((void *)ints, _int, 1, 2);

    printf("%d\n", value);
}

Здесь мы видим заражение. Нам нужно возвращаемое значение, но мы не знаем тип, поэтому мы должны вернуть указатель void. Это означает, что нам нужно выделить память правильного типа. И нам нужно получить доступ к массиву с правильным типом, большим количеством избыточности, большим количеством типов преобразования. И затем вызывающий должен связываться с кучей типов:

Что за беспорядок.

Мы можем убрать часть избыточности с помощью макросов.

#define get_idx(a,t,i) ((t *){a}[i])
#define make_var(t) ((t *)malloc(sizeof(t)))

void *add(void * array, enum Types type, size_t idx1, size_t idx2) {
    switch(type) {
        case _int : {
            int *sum = make_var(int);
            *sum = get_idx(array, int, idx1) + get_idx(array, int, idx2);
            return sum;
        };
        case _double : {
            double *sum = make_var(double);
            *sum = get_idx(array, double, idx1) + get_idx(array, double, idx2);
            return sum;
        };
    }; 
}

You возможно, уменьшит избыточность с помощью еще большего количества макросов, например ответ Патрика , но мальчик быстро превращается в макро-ад. В определенный момент вы больше не кодируете в C, поскольку вы быстро расширяете пользовательский язык, реализованный с помощью стеков макросов.

Очень умная идея Клиффорда об использовании размеров вместо типов поможет не работает здесь. Чтобы действительно что-то делать со значениями, нам нужно знать их типы.


Еще раз, я не могу express достаточно сильно определить, насколько велик полиморфизм смолы в C.

3 голосов
/ 21 января 2020

Вместо передачи идентификатора типа достаточно и проще передать размер объекта:

void test_function( size_t sizeof_type, void* value, void* array ) 
{
    size_t element_index = 0 ; // for example

    memcpy( (char*)array + element_index * sizeof_type, value, sizeof_type ) ; 
}
1 голос
/ 21 января 2020

Чтобы оставаться агрегированными по типу c и поддерживать гибкость использования, которую вы, по-видимому, хотите, вам нужно переместить «основной код» в макрос и вызывать его для каждого случая:

typedef enum {
    USE_TYPE_INT = 0,
    USE_TYPE_DOUBLE = 1,
    // ...
} USE_TYPE;

void test_function(USE_TYPE use_type, void * value, void * array) {

#define TEST_FUNCTION_T(type) do { \
    type * valueT = value;         \
    type * arrayT = array;         \
    /* Main code of the program */ \
    arrayT[0] = *valueT;           \
    /* ... */                      \
} while(0)

    // Set types to the parameters based on 'use_type'
    switch (use_type) {
        case USE_TYPE_INT:
            TEST_FUNCTION_T(int);
            break;
        case USE_TYPE_DOUBLE:
            TEST_FUNCTION_T(double);
            break;
        // ...
    }

#undef TEST_FUNCTION_T

}

Обратите внимание, что, хотя вы определяете макрос TEST_FUNCTION_T только один раз, каждое использование приведет к дублированию блока кода, отличающемуся только type, вставленным в вызов макроса при компиляции программы.

1 голос
/ 21 января 2020

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

void test_function(int use_type, void * value, void * array) {
    // Set types to the parameters based on 'use_type'
    if (use_type == 0) { // Int type
        int * valueT = value, *arrayT = array; //the casts in C are unnecessary
        arrayT[0] = *valueT;
    } else if (use_type == 1) { // Double type
        double * valueT = value, *arrayT = array;
        arrayT[0] = *valueT;
    }
}

, но вы, вероятно, должны делать это inline, без перевода типа <-> int :

(type*){array}[0] = *(type*){value} //could make it DRY with a macro
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...