实现不同但相似的结构/功能集而不需要复制粘贴

9
我正在为C语言实现一组常见但不是那么琐碎(或容易出错)的数据结构(这里),并想到了一个让我思考的想法。
简单来说,问题是如何在不必复制/重写算法的情况下实现使用相似算法但具有不同接口的两个结构? 最好的方式是什么,指的是最易于维护和调试的方法。
我认为显而易见的是,您不希望有两份相同算法的副本。
动机:
假设您有一个结构(称之为“map”),其中包含一组相关函数(“map_*()”)。 由于地图需要将任何内容映射到任何内容,因此我们通常会采用“void * key”和“void * data”来实现它。 但是,请考虑将int映射到int的地图。 在这种情况下,您需要将所有键和数据存储在另一个数组中,并将其地址提供给地图,这不太方便。
现在想象一下是否有类似的结构(称之为“mapc”,其中c代表“copies”),在初始化期间需要采用“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和的数组。

我的想法

在上面的示例中,mapmapc非常相似。如果你考虑一下,mapset结构和函数也非常相似。我已经想到了以下几种方法来实现它们的算法,只需实现一次即可用于所有情况。但是,它们都不太令我满意。

  1. Use macros. Write the function code in a header file, leaving the structure dependent stuff as macros. For each structure, define the proper macros and include the file:

    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"
    

    This method is not half bad, but it's not so elegant.

  2. Use function pointers. For each part that is dependent on the structure, pass a function pointer.

    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);
    }
    

    This method requires writing more functions that could have been avoided in the macro method (as you can see, the code here is longer) and doesn't allow optimizers to inline the functions (as they are not visible to map_generic.c file).

那么,您如何实现这样的功能?

注:我是在 stack-overflow 的问题形式中编写代码的,所以如果有小错误,请原谅。

附带问题:有没有更好的后缀来表示“此结构体复制数据而不是指针”? 我使用 c 表示“copy”,但可能还有更好的英文单词我不知道。


更新:

我想出了第三个解决方案。 在这个解决方案中,仅编写一个版本的 map,即保留数据副本的版本(mapc)。此版本将使用 memcpy 复制数据。另一个 map 是对此的接口,它采用 void *keyvoid *data 指针,并将 &key&data 发送到 mapc,以便复制它们包含的地址(使用 memcpy)。

这种解决方案的缺点是通过 memcpy 进行普通指针赋值,但它完全解决了其他问题,并且非常干净。

或者,可以只实现 map 并使用一个额外的带有 mapcvectorc,它首先将数据复制到向量中,然后将地址提供给 map。这会产生副作用,即从 mapc 中删除要比其他结构慢得多,或者留下垃圾(或需要其他结构来重用垃圾)。


更新2:

我得出结论,粗心的用户可能会像编写 C++ 一样使用我的库,不停地复制。因此,我放弃了这个想法,只接受指针。


1
那个问题超过了120行。ಠ___ಠ - Hans Z
@hans,我想确保我不会得到一个适合新手的答案。我的意思是,我想确保你明白我已经仔细考虑过了。 - Shahbaz
1
终于在SO上看到一个好的、有意义的非n00b问题。+1。 - user529758
如果您有更多的标签,可能会得到更多的赞。 - Marcin
3个回答

3

您大致介绍了两种可能的解决方案。

预处理器宏大致对应于C++模板,并具有相同的优缺点:

  • 它们难以阅读。
  • 复杂的宏通常很难使用(考虑参数的类型安全性等)
  • 它们仅是更多代码的“生成器”,因此在编译输出中仍存在许多重复性。
  • 另一方面,它们允许编译器优化大量内容。

函数指针大致对应于C ++多态性,它们是我认为更清晰且通常更易于使用的解决方案,但它们会在运行时带来某些成本(对于紧密循环,几个额外的函数调用可能很昂贵)。

除非性能真正关键,否则我通常更喜欢函数调用。


当我写第一个方法时,我正好在考虑模板,但我没有注意到第二个方法是多态的! - Shahbaz
C++中的多态性基于虚函数。每个具有虚函数(直接或间接通过继承)的类都有vtable。而vtable实际上就是(按C术语)一个仅由函数指针作为其成员的结构。 - mity
是的,我熟悉C++的内部机制 ;) - Shahbaz

1

多态性类似于我的第二种方法,正如mity所建议的那样,但是按照那些链接中的方式做太像C++了。我认为这个问题可以更像C语言一样处理。 - Shahbaz

1
还有第三种选项,您没有考虑到:您可以创建一个外部脚本(用另一种语言编写),从一系列模板中生成您的代码。这类似于宏方法,但您可以使用像Perl或Python这样的语言来生成代码。由于这些语言比C预处理器更强大,因此您可以避免通过宏进行模板化时可能存在的一些潜在问题。我曾在需要使用类似于您示例#1中的复杂宏的情况下使用过此方法。最终,结果比使用C预处理器更少出错。缺点是,在编写生成器脚本和更新makefile之间,初始设置可能会更加困难(但在我看来最终值得)。

这非常有趣,确实没有想到过。生成的代码是否需要适当缩进或处理 '\' 等情况,会不会变得很丑陋? - Shahbaz
@Shahbaz- 通常情况下,使生成的代码在样式上与其余代码匹配并不是很重要。由于代码是作为构建过程的一部分生成的,因此它并不一定是为人类消费而设计的(只有编译器)。如果您想使其可读性更好,可以使用像astyleuncrustify这样的工具自动重新格式化代码以匹配所需的样式。至于反斜杠,您可以轻松地编写脚本来查找输入代码中的反斜杠并根据需要连接行等。 - bta
@Shahbaz- 为了调试目的,是的,你需要使用类似 astyle 的工具来使你的代码更易读(我发现它甚至对人类生成的代码也有帮助)。我的观点是,通常只有在需要时才会这样做。你不会将生成的代码提交到你的存储库中,在同一个文件中混合生成的代码和非生成的代码,或者类似的操作。 - bta
好的,我会考虑一下。也许在我的情况下,宏解决方案并不太复杂,但你的解决方案肯定更具可扩展性。实际上,脚本可以从文件中读取模板并生成源文件,而不是自己生成所有内容,这使得编写模板更容易。 - Shahbaz
我接受了你的答案,因为它实际上给了我一个新的想法,尽管我不会使用它。如果将来再遇到这样的问题,这肯定是我考虑的事情。 - Shahbaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接