使用 if/else 中几乎相同的语句减少 C 程序中的代码重复?

Reducing code duplication in C program with nearly identical statements in if/else?

我正在尝试减少我的 C 程序中的代码重复,其中 if/else 块的每个分支中的所有语句都是相同的,除了函数名称及其参数。这个想法是用户指定 xyz,然后程序测量 运行 func_xfunc_y,或 func_z 1000 次。

更具体地说,这是 C 代码的高级设计:

// struct definitions
struct dat_x {...};
struct dat_y {...};
struct dat_z {...};

// reading structs from a text file
struct dat_x read_dat_x_from_file(char *path);
struct dat_y read_dat_y_from_file(char *path);
struct dat_z read_dat_z_from_file(char *path);

// functions
int func_x(struct dat_x);
int func_y(struct dat_y);
int func_z(struct dat_z);

// runner computing runtime of func_x, func_y, or func_z
int main(int argc, char** argv) {
    char *func_name = argv[1];
    char *path = argv[2];

    int a;
    clock_t t;

    if (strcmp(func_name, "x") == 0) {
        struct dat_x args = read_dat_x_from_file(path);

        t = clock();
        for (int i = 0; i < 1000; i++) {
            a += func_x(args);
        }
        t = clock() - t;

    } else if (strcmp(func_name, "y") == 0) {
        struct dat_y args = read_dat_y_from_file(path);

        t = clock();
        for (int i = 0; i < 1000; i++) {
            a += func_y(args);
        }
        t = clock() - t;

    } else if (strcmp(func_name, "z") == 0) {
        struct dat_z args = read_dat_z_from_file(path);

        t = clock();
        for (int i = 0; i < 1000; i++) {
            a += func_z(args);
        }
        t = clock() - t;

    }

    // report runtime
    double e = ((double)t) / CLOCKS_PER_SEC;
    printf("%s: %f %d\n", func_name, e, a);
}

如您所见,在 main 函数中,if-else 块的每个分支中的所有语句都是相同的;唯一的区别是 func_xfunc_yfunc_z.

在函数式语言中,此模式可以通过具有函数 run_timing_benchmark 来抽象,该函数接受 func_*dat_* 参数,并在 运行 中循环 (可能使用多态性来定义 g 的签名)。虽然我可以在 C 中使用函数指针,但我不能编写多态类型签名。

对于如何减少程序中的重复,使定时代码只定义一次,有什么建议?在实践中我可能有几十个函数(不仅仅是x/y/z)使用相同的代码进行基准测试,并且时序代码可能更复杂。

一种不太优雅的方法是使用这样的函数指针:

int (*funcPtr)(void *arg)

并且在函数的实现中,您将 void * 转换为实际的函数参数,例如 struct dat_x *arg = (struct dat_x *)arg

最好的方法是为函数提供相同的接口。然后你可以创建一个函数指针数组并获得非常漂亮的代码。

但是,如果您坚持使用函数,减少代码重复的最不邪恶的方法是使用类似函数的宏。例如通过使用 C11 _Generic:

#define read_dat_from_file(result, path) (result) =  \
  _Generic((result),                                 \
    struct dat_x: read_dat_x_from_file,              \
    struct dat_y: read_dat_y_from_file,              \
    struct dat_z: read_dat_z_from_file ) (path);

其中 result 是您要在其中存储结果的结构类型的变量。完整示例:

#include <stdio.h>

struct dat_x { int x; };
struct dat_y { int y; };
struct dat_z { int z; };

struct dat_x read_dat_x_from_file(char *path) 
{ 
  puts(__func__); 
  return (struct dat_x){1};
}

struct dat_y read_dat_y_from_file(char *path)
{ 
  puts(__func__); 
  return (struct dat_y){2};
}

struct dat_z read_dat_z_from_file(char *path)
{ 
  puts(__func__); 
  return (struct dat_z){3};
}


#define read_dat_from_file(result, path) (result) =  \
  _Generic((result),                                 \
    struct dat_x: read_dat_x_from_file,              \
    struct dat_y: read_dat_y_from_file,              \
    struct dat_z: read_dat_z_from_file ) (path);

int main (void)
{
  struct dat_x x;
  struct dat_y y;
  struct dat_z z;

  read_dat_from_file(x, "");
  read_dat_from_file(y, "");
  read_dat_from_file(z, "");

  printf("%d\n", x.x);
  printf("%d\n", y.y);
  printf("%d\n", z.z);
}

输出:

read_dat_x_from_file
read_dat_y_from_file
read_dat_z_from_file
1
2
3

一个想法可能是建立一个联合来抽象函数签名和 return 值的差异。然后建一个table的函数,根据传入的名字调用正确的函数。像这样的东西:(警告:未经测试!)

// struct definitions
struct dat_x {...};
struct dat_y {...};
struct dat_z {...};

// Build a union that contains the structs
union uargs
{
    struct dat_x;
    struct dat_y;
    struct dat_z;
};

// reading structs from a text file (but packaged into the union type)
union uargs read_dat_x_from_file(char *path);
union uargs read_dat_y_from_file(char *path);
union uargs read_dat_z_from_file(char *path);

// functions
int func_x(union uargs dat);
int func_y(union uargs dat);
int func_z(union uargs dat);

struct table_t
{
    char *name;
    union uargs (*read_dat_fp);
    int (*fp)(union uargs dat);
};

// Table of function pointers
struct table_t func_table[]
{
    { "x", read_dat_x_from_file, func_x},
    { "y", read_dat_y_from_file, func_y},
    { "z", read_dat_x_from_file, func_z}
};

// runner computing runtime of func_x, func_y, or func_z
int main(int argc, char** argv) {
    char *func_name = argv[1];
    char *path = argv[2];

    int a;
    clock_t t;

    for(int i = 0; i < sizeof(func_table) / sizeof(table_t); i++)
    {
        if(strcmp(func_name, func_table[i].name) == 0)
        {
            union uargs args = func_table[i].read_dat_fp(path);
            t = clock();
            for (int i = 0; i < 1000; i++) {
                a += func_table[i].fp(args);
            }
            t = clock() - t;
            break;
        }
    }

    // report runtime
    double e = ((double)t) / CLOCKS_PER_SEC;
    printf("%s: %f %d\n", func_name, e, a);
}

这消除了代码重复并且在某种程度上也易于扩展。 另一种选择可能是使用一些宏魔法,如来自@Barmar 的

Edit:当然,您可以简单地使用 void* 和类型转换来传递指向结构的指针,并根据需要重新转换它们,而不是联合在函数里面。但是你完全抛弃了所有类型检查。

您可以使用宏来生成代码。由于所有结构和函数都遵循通用的命名方案,因此您可以使用令牌粘贴来生成它们。

#define PROCESS(suffix, func_name_var, path_var, sum_var, time_var) \
time_var = time();
if(strcmp(func_name_var, #suffix) == 0) { \
    struct dat_##suffix args = read_dat_##suffix##_from_file(path_var); \
    for (int i = 0; i < 1000; i++) { \
        sum_var += func_##suffix(args); \
    } \
time_var = time() - time_var;

那么你可以这样使用它:

        PROCESS(x, func_name, path, a, t)
        else PROCESS(y, func_name, path, a, t)
        else PROCESS(z, func_name, path, a, t)

@MarcoBonelli 在评论中正确地观察到您的函数并不像它们看起来那么相似。它们具有不同的参数和 return 类型,这是 C 等强类型语言中的一个重要区别。这些函数在 C 语言意义上是不可互换的;鉴于它们具有不同的 return 类型,甚至没有任何函数指针类型可以与指向所有函数的指针兼容。

如果您可以更改功能,那么就有可能以克服该限制的方式进行更改。例如,您可以接受要填充的结构作为 void *:

类型的输出参数
void read_dat_y_from_file(const char *path, void *args) {
    struct dat_y *y_args = (struct dat_y *) args;
    // ...
}

    // ...
    struct dat_y args;
    read_dat_y_from_file(path, &args);

您可以围绕它编写一个基于函数指针的解决方案。


但是不需要修改任何函数的更简单的方法是将重复的代码移动到宏中:

#define read_and_time(tag) do { \
    struct dat_ ## tag args = read_dat_## tag ## _from_file(path); \
    t = clock(); \
    for (int i = 0; i < 1000; i++) { \
        a += func_ ## tag(args); \
    } \
    t = clock() - t; \
while (0)

这样,您可以将 if / else 链减少到

if (strcmp(func_name, "x") == 0) {
    read_and_time(x);
} else if (strcmp(func_name, "y") == 0) {
    read_and_time(y);
} else if (strcmp(func_name, "z") == 0) {
    read_and_time(z);
}

您甚至可以将更多内容拉入宏中,但我认为这种形式最清楚。