我正在为C(here)实现一组常见但又不那么微不足道(或容易出错)的数据结构,并且提出了让我思考的想法。
简而言之,问题是,实现两个使用类似算法但具有不同接口的结构的最佳方法是什么?无需复制粘贴/重写算法?最好的,我的意思是最易维护和可调试的。
我认为很明显为什么你不想要同一算法的两个副本。
假设你有一组结构(称之为map
),并带有一组相关的函数(map_*()
)。由于地图需要将任何内容映射到任何内容,我们通常会使用void *key
和void *data
来实现它。但是,请考虑int
到int
的地图。在这种情况下,您需要将所有键和数据存储在另一个数组中,并将其地址提供给map
,这不太方便。
现在假设有一个类似的结构(称之为mapc
,c表示“副本”),在初始化过程中需要sizeof(your_key_type)
和sizeof(your_data_type)
以及给定void *key
和{ {1}}在插入时,它会使用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
和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"
此方法需要编写宏方法中可以避免的更多函数(如您所见,这里的代码更长)并且不允许优化器内联函数(因为它们对{{1不可见)文件)。
那么,你会如何实现这样的东西呢?
注意:我在堆栈溢出问题表单中编写了代码,如果有小错误,请原谅。
附带问题:任何人都有一个更好的想法,后缀说“这个结构复制数据而不是指针”?我使用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
首先将数据复制到vector,然后将地址提供给mapc
。这有副作用,即从map
删除要么大得多,要么留下垃圾(或要求其他结构重用垃圾)。
我得出的结论是,粗心的用户可能会像编写C ++一样使用我的库,复制后复制后复制。因此,我放弃了这个想法,只接受指针。
答案 0 :(得分:3)
您大致涵盖了两种可能的解决方案。
预处理器宏大致对应于C ++模板,具有相同的优点和缺点:
函数指针大致对应于C ++多态,它们是IMHO更清晰且通常更易于使用的解决方案,但它们在运行时带来了一些成本(对于紧密循环,很少额外的函数调用可能很昂贵)。
我通常更喜欢函数调用,除非性能非常关键。
答案 1 :(得分:1)
您正在寻找的是多态性。 C ++,C#或其他面向对象的语言更适合此任务。虽然很多人试图在C中实现多态行为。
Code Project有一些关于这个主题的好文章/教程:
http://www.codeproject.com/Articles/10900/Polymorphism-in-C
http://www.codeproject.com/Articles/108830/Inheritance-and-Polymorphism-in-C
答案 2 :(得分:1)
您还没有考虑过第三个选项:您可以创建外部脚本(用其他语言编写)以从一系列模板生成代码。这与宏方法类似,但您可以使用Perl或Python等语言生成代码。由于这些语言比C预处理器更强大,因此可以避免通过宏执行模板时固有的一些潜在问题。我曾经尝试过使用这种方法,我很想使用像你的例子#1那样复杂的宏。最后,事实证明它比使用C预处理器更不容易出错。缺点是在编写生成器脚本和更新makefile之间,最初设置起来有点困难(但最终IMO值得)。