【问题标题】:Reducing code duplication in C program with nearly identical statements in if/else?在 if/else 中使用几乎相同的语句减少 C 程序中的代码重复?
【发布时间】:2019-09-13 11:33:32
【问题描述】:

我试图减少我的 C 程序中的代码重复,其中 if/else 块的每个分支中的所有语句都是相同的,除了函数名及其参数。这个想法是用户指定xyz,程序测量运行func_xfunc_yfunc_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_* 参数并且hten 运行循环(可能使用多态性来定义g 的签名)。虽然我可以在 C 中使用函数指针,但我不能编写多态类型签名。

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

【问题讨论】:

  • 是更改功能的一种选择,还是您坚持使用它们?
  • Welp,你的函数都有不同的签名,如果不改变这些,真的做不了什么。
  • 如果您有完整的工作代码,那么 codereview.stackexchange.com 是一个更好的地方。
  • @klutt 那么 codereview.stackexchange.com 是一个更好的地方 我倾向于同意,但考虑到它是"a practical, answerable problem that is unique to software development",这并不是题外话。而且我怀疑这里的知名度比在 codereview.stackexchange.com 上要高得多

标签: c design-patterns code-duplication


【解决方案1】:

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

#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)

【讨论】:

  • 感谢您的出色回答。为什么你更喜欢使用PROCESS(x, ...) 而不是PROCESS("x", ...),也就是说,将后缀作为字符串传递不是更容易吗?
  • 因为它需要使用令牌粘贴来创建像dat_xread_dat_x_from_file 这样的名称。这不适用于字符串。
【解决方案2】:

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

如果您可以更改功能,则可以通过克服该限制的方式进行更改。例如,您可以接受要填充的结构作为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);
}

您甚至可以在宏中添加更多内容,但我认为这种形式最清晰。

【讨论】:

  • 哦,我总是忘记## 的存在。不错的宏!
  • +1 感谢@John Bollinger 提供了涵盖void 解决方案和macro 解决方案的出色答案。
【解决方案3】:

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

// 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 的this answer

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

【讨论】:

  • “变体”非常难看。假设dat_x 很大但dat_y 很小?你会扼杀一切表现。在我看来,这会产生一堆原始代码中不存在的新问题。
  • @Lundin 可能超出此答案范围的进一步优化。例如传递指针而不是巨大的结构。我显然没有在这个答案中解决这些问题。
【解决方案4】:

最好的方法是为函数提供相同的界面。然后你就可以创建一个函数指针数组并得到相当漂亮的代码。

但是,如果您坚持使用函数,那么减少代码重复的最不邪恶的方法是使用类似函数的宏。例如通过使用 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

【讨论】:

  • 这并没有解决 OP 最初的担忧——if-else 有很多代码重复。
【解决方案5】:

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

int (*funcPtr)(void *arg)

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

【讨论】:

  • 一切都很酷,除了每个函数都需要首先使用其他函数系列获取数据,这些函数都返回不同的值。
  • 同样,使用 void * 作为信息的载体。无类型工作并不好,但它可以工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-01-22
  • 2012-05-11
  • 1970-01-01
  • 2021-11-09
  • 1970-01-01
  • 2011-11-21
  • 1970-01-01
相关资源
最近更新 更多