Реализация различных, но схожих структурных/функциональных наборов без копирования-вставки

Я реализую набор общих, но не столь тривиальных (или подверженных ошибкам) ​​структур данных для C (здесь) и только что пришел с идеей, которая заставила меня задуматься.

Короче говоря, что является лучшим способом реализовать две структуры, которые используют похожие алгоритмы, но имеют разные интерфейсы, не имея необходимости копировать/вставлять/переписывать алгоритм?. В лучшем случае, я имею в виду большинство поддерживаемых и отлаживаемых.

Я думаю, что очевидно, почему вы не хотели бы иметь две копии одного и того же алгоритма.

Мотивация

Скажите, что у вас есть структура (назовите ее map) с набором связанных функций (map_*()). Так как карта должна отображать что угодно, мы обычно реализуем ее с помощью void *key и void *data. Однако подумайте о карте от int до int. В этом случае вам нужно будет хранить все ключи и данные в другом массиве и указывать их адреса на map, что не так удобно.

Теперь представьте, была ли подобная структура (назовите ее mapc, c для "копий" ), которая во время инициализации принимает sizeof(your_key_type) и sizeof(your_data_type) и задана void *key и void *data при вставке, она будет использовать memcpy, чтобы скопировать ключи и данные на карте, а не просто удерживать указатели. Пример использования:

int i;
mapc m;
mapc_init(&m, sizeof(int), sizeof(int));
for (i = 0; i < n; ++i)
{
    int j = rand();  /* whatever */
    mapc_insert(&m, &i, &j);
}

что довольно приятно, потому что мне не нужно сохранять еще один массив из i и j s.

Мои идеи

В приведенном выше примере map и mapc очень тесно связаны. Если вы думаете об этом, структуры и функции map и set также очень похожи. Я подумал о следующих способах реализации их алгоритма только один раз и использовать его для всех из них. Однако ни один из них не удовлетворяет меня.

  • Использовать макросы. Напишите код функции в файле заголовка, оставив зависимые от структуры элементы как макросы. Для каждой структуры определите правильные макросы и включите файл:

    map_generic.h
    
    #define INSERT(x) x##_insert
    
    int INSERT(NAME)(NAME *m, PARAMS)
    {
        // create node
        ASSIGN_KEY_AND_DATA(node)
        // get m->root
        // add to tree starting from root
        // rebalance from node to root
        // etc
    }
    
    map.c
    
    #define NAME map
    #define PARAMS void *key, void *data
    #define ASSIGN_KEY_AND_DATA(node) \
    do {\
        node->key = key;\
        node->data = data;\
    } while (0)
    #include "map_generic.h"
    
    mapc.c
    
    #define NAME mapc
    #define PARAMS void *key, void *data
    #define ASSIGN_KEY_AND_DATA(node) \
    do {\
        memcpy(node->key, key, m->key_size);\
        memcpy(node->data, data, m->data_size);\
    } while (0)
    
    #include "map_generic.h"
    

    Этот метод не слишком плох, но он не настолько изящный.

  • Используйте указатели на функции. Для каждой части, которая зависит от структуры, передайте указатель на функцию.

    map_generic.c
    
    int map_generic_insert(void *m, void *key, void *data,
        void (*assign_key_and_data)(void *, void *, void *, void *),
        void (*get_root)(void *))
    {
        // create node
        assign_key_and_data(m, node, key, data);
        root = get_root(m);
        // add to tree starting from root
        // rebalance from node to root
        // etc
    }
    
    map.c
    
    static void assign_key_and_data(void *m, void *node, void *key, void *data)
    {
        map_node *n = node;
        n->key = key;
        n->data = data;
    }
    
    static map_node *get_root(void *m)
    {
        return ((map *)m)->root;
    }
    
    int map_insert(map *m, void *key, void *data)
    {
        map_generic_insert(m, key, data, assign_key_and_data, get_root);
    }
    
    mapc.c
    
    static void assign_key_and_data(void *m, void *node, void *key, void *data)
    {
        map_node *n = node;
        map_c *mc = m;
        memcpy(n->key, key, mc->key_size);
        memcpy(n->data, data, mc->data_size);
    }
    
    static map_node *get_root(void *m)
    {
        return ((mapc *)m)->root;
    }
    
    int mapc_insert(mapc *m, void *key, void *data)
    {
        map_generic_insert(m, key, data, assign_key_and_data, get_root);
    }
    

    Этот метод требует записи большего количества функций, которые можно было бы избежать в методе макросов (как вы можете видеть, код здесь длиннее) и не позволяет оптимизаторам встроить функции (поскольку они не видны для map_generic.c файл).

Итак, как бы вы начали реализовывать что-то вроде этого?

Примечание. Я написал код в форме вопроса о переполнении стека, поэтому извините меня, если есть незначительные ошибки.

Боковой вопрос: у кого-то есть лучшая идея для суффикса, который говорит, что "эта структура копирует данные вместо указателя"? Я использую c, который говорит "копии", но на английском может быть гораздо лучшее слово, о котором я не знаю.


Обновление:

У меня появилось третье решение. В этом решении записывается только одна версия map, которая хранит копию данных (mapc). Эта версия использовала бы memcpy для копирования данных. Другой map является интерфейсом к этому, принимая указатели void *key и void *data и отправляя &key и &data в mapc так, чтобы адрес, который они содержат, был скопирован (используя memcpy).

Это решение имеет недостаток, что нормальное назначение указателя выполняется memcpy, но оно полностью решает проблему в противном случае и очень чистое.

В качестве альтернативы можно реализовать только map и использовать дополнительный vectorc с mapc, который сначала копирует данные в вектор, а затем передает адрес map. Это имеет побочный эффект, что удаление из mapc будет либо значительно медленнее, либо оставить мусор (или потребовать от других структур повторного использования мусора).


Обновление 2:

Я пришел к выводу, что неосторожные пользователи могут использовать мою библиотеку так, как они пишут С++, копировать после копирования после копирования. Поэтому я отказываюсь от этой идеи и принимаю только указатели.

Ответ 1

Также есть третий вариант, который вы не учли: вы можете создать внешний script (написанный на другом языке), чтобы сгенерировать свой код из серии шаблонов. Это похоже на метод макросов, но вы можете использовать язык Perl или Python для генерации кода. Поскольку эти языки более мощные, чем предварительный процессор C, вы можете избежать некоторых потенциальных проблем, присущих шаблонам с помощью макросов. Я использовал этот метод в тех случаях, когда у меня возникло соблазн использовать сложные макросы, как в вашем примере # 1. В конце концов, он оказался менее подверженным ошибкам, чем с использованием препроцессора C. Недостатком является то, что между написанием генератора script и обновлением make файлов его немного сложнее получить сначала (но IMO стоит в конце).

Ответ 2

Вы грубо освещали оба возможных решения.

Макросы препроцессора примерно соответствуют шаблонам С++ и имеют те же преимущества и недостатки:

  • Их трудно читать.
  • Сложные макросы часто трудно использовать (рассмотрите тип безопасности параметров и т.д.).
  • Они просто "генераторы" большего количества кода, поэтому в скомпилированном выпуске все еще существует много двуличности.
  • С другой стороны, они позволяют компилятору оптимизировать много вещей.

Указатели функций примерно соответствуют полиморфизму С++, и они являются более чистым и, как правило, простым в использовании решением IMHO, но они приносят некоторую стоимость во время выполнения (для жестких циклов несколько дополнительных вызовов функций могут быть дорогими). ​​

Я обычно предпочитаю вызовы функций, если производительность не очень важна.

Ответ 3

То, что вы ищете, это полиморфизм. С++, С# или другие объектно-ориентированные языки более подходят для этой задачи. Хотя многие люди пытались реализовать полиморфное поведение в C.

В проекте Code есть хорошие статьи/учебные пособия по теме:

http://www.codeproject.com/Articles/10900/Polymorphism-in-C

http://www.codeproject.com/Articles/108830/Inheritance-and-Polymorphism-in-C