使用 GCC 在 C 中进行函数重载 - 具有多个参数的函数

Function overloading in C using GCC - functions with mutiple arguments

在上一个问题中,我找到了一种在 C99 中重载函数的方法,当每个函数只接受一个参数时。有关详细信息,请参阅 中的答案。

既然我已经找到了一种使用单参数函数的方法,我想知道如何为采用多个参数的函数完成此操作。我认为它与 __VA_ARGS__ 和使用 ... 有关,但我似乎找不到任何有用的东西,甚至无法编译。


这适用于带有 2 个参数的打印:

#define print(x, y)                                                            \
__builtin_choose_expr(__builtin_types_compatible_p(typeof(x), int) &&          \
                      __builtin_types_compatible_p(typeof(y), int), print_int, \
(void)0)(x, y)

但是如果我还想要另一个接受一个参数的版本,我就不能重新定义它。添加这个会给我一个错误,说 print is redefined:

#define print(x)                                                                     \
__builtin_choose_expr(__builtin_types_compatible_p(typeof(x), char[]), print_string, \
(void)0)(x)

如何重载 print 以便它可以使用 2 个整数作为输入或使用字符数组?

用法示例:

print(1, 2);
print("this");

甚至更好...我怎样才能使它适用于任意类型组合或任意数量的参数?

还要记住,因为这是 C99,所以 _Generic 关键字不可用。

此解决方案绝不是通用的,但它可以针对问题中提出的非常具体的情况完成工作。

#include <stdio.h>

#define print(...) \
        __builtin_choose_expr(__builtin_types_compatible_p(typeof(FIRST(__VA_ARGS__)), int), print_int, print_string)\
(__VA_ARGS__)

#define FIRST(A, ...) A

void print_int(int i, int j) {
    printf("int: %d %d\n", i, j);
}

void print_string(char* s) {
    printf("char*: %s\n", s);
}

int main(int argc, char* argv[]) {

    print(1, 2);
    print("this");

    return 0;
}

如果有人能找到更通用的解决方案,并且在添加新重载时能够始终如一地工作,我们将不胜感激。

您可以使用 GCC 的扩展和大量的预处理器技巧来做您想做的事。评论者已经表明了他们的观点:C 是相当明确的,并且与产生的符号具有一对一的关系。如果您想要函数重载和类型检查,请使用提供它们的众多语言之一。

巴洛克宏解决方案往往是玩具而不是适合生产的代码,但挑战极限仍然是一项有趣的练习。不过,请注意安全,并注意:

  • ...该解决方案不可移植,因为通过类型选择参数的核心技巧已经是特定于 GCC 的。
  • ...解决方案基于宏。查找宏中的语法错误很困难,因为错误消息指的是用户看不到的扩展代码。
  • ...解决方案用许多宏名称污染了命名空间。如果你真的想使用这个解决方案,请为你的所有宏(最明显的宏除外)添加前缀,以尽量减少符号冲突的危险。

顺便说一句,让我们实现一个函数 put,根据其类型将其参数写入 stdin

const char *name = "Fred";
double C = 12.5;

put(1, " ", 2);                         // 1 2
put("Hello, I'm ", name, "!");          // Hello, I'm Fred!
put(C, " Celsius");                     // 12.5 Celsius
put(C * 1.8 + 32.0, " Fahrenheit");     // 54.5 Fahrenheit

为了简单起见,该解决方案最多只接受 intconst char *double 三个参数,但参数的最大数量是可扩展的。

解决方案包括以下部分:

可变常量类型宏

假设您想要一个对所有参数求和的函数。参数的数量可能会有所不同,但所有参数的类型都是 double。如果它们不是 double 类型,则应提升为 double.

可变参数函数不是一个好的解决方案,因为它们会将参数传递给每个单独类型的函数。尝试 sum(1, 2, 3)double 将导致灾难性的后果。

相反,您可以使用复合文字即时创建一个 double 数组。使用sizeof机制获取数组的长度。 (参数可能有副作用,因为 sizeof 中的数组未被评估,仅确定其大小。)

#define sum(...) sum_impl(sizeof((double[]){__VA_ARGS__})/ \
                 sizeof(double), (double[]){__VA_ARGS__})

double sum_impl(size_t n, double x[])
{
    double s = 0.0;

    while (n--) s += x[n];
    return s;
}

这将在 doubles 上执行的计算中为 sum(1, 2, 3) 产生 6.0

变体类型

您希望所有参数都是同一类型,但该类型应该能够代表您的函数的所有支持类型。创建变体的 C 方法是使用标记联合,unionstruct:

typedef struct var_t var_t;

struct var_t {
    int type;
    union {
        int i;
        double f;
        const char *s;
    } data;
};

类型可以是枚举。我在这里根据 printf 格式使用字符常量。

表达式的变体由宏 VAR 决定,它本质上是您在上面发布的特定 gcc:

#define CHOOSE __builtin_choose_expr
#define IFTYPE(X, T) __builtin_types_compatible_p(typeof(X), T)

#define VAR(X)                                          \
    CHOOSE(IFTYPE(X, int),          make_var_i,         \
    CHOOSE(IFTYPE(X, const char[]), make_var_s,         \
    CHOOSE(IFTYPE(X, const char *), make_var_s,         \
    CHOOSE(IFTYPE(X, double),       make_var_f,         \
                                    make_var_0))))(X)

宏调用任何 make_var 函数。必须为每个有效类型定义这些函数:

var_t make_var_i(int X)         { var_t v = {'i', {.i = X}}; return v; }
var_t make_var_s(const char *X) { var_t v = {'s', {.s = X}}; return v; }
var_t make_var_f(double X)      { var_t v = {'f', {.f = X}}; return v; }
var_t make_var_0()              { var_t v = {'#'}; return v; }

X 合并到依赖于类型的表达式中不起作用,正如您已经发现的那样。您也不能在这里使用带有指定初始化程序的复合文字,可能出于相同的原因。 (我说过用宏检查错误很困难,不是吗?)

这是唯一的 GCC 特定部分;它也可以通过 C11 的 _Generic.

来实现

将宏应用于函数的所有参数

您必须将 VAR 宏应用于可变参数 put 宏的所有参数。在得到一个空列表之前,你不能处理可变参数的头部,因为你不能递归地扩展宏,但是你可以使用一个技巧来计算宏的参数,然后扩展到一个有那么多参数的宏:

#define PUT1(_1)            put_impl(1, (var_t[]){VAR(_1)})
#define PUT2(_1, _2)        put_impl(2, (var_t[]){VAR(_1), VAR(_2)})
#define PUT3(_1, _2, _3)    put_impl(3, (var_t[]){VAR(_1), VAR(_2), VAR(_3)})

#define SELECT_N(_1, _2, _3, N, ...) N

#define put(...) SELECT_N(__VA_ARGS__, PUT3, PUT2, PUT1)(__VA_ARGS__)

现在 put 接受 1、2 或 3 个参数。如果您提供超过 3 个,您会收到一条模糊的错误消息,该消息与没有提供太多参数没有任何关系。

上面的代码不接受空参数列表。使用 GCC entension , ##__VA_ARGS,仅当 variadicargument 列表不为空时才会写一个逗号,您可以将其扩展为:

#define PUT0()              put_impl(0, NULL)
#define PUT1(_1)            put_impl(1, (var_t[]){VAR(_1)})
#define PUT2(_1, _2)        put_impl(2, (var_t[]){VAR(_1), VAR(_2)})
#define PUT3(_1, _2, _3)    put_impl(3, (var_t[]){VAR(_1), VAR(_2), VAR(_3)})

#define SELECT_N(X, _1, _2, _3, N, ...) N

#define put(...) SELECT_N(X, ##__VA_ARGS__, PUT3, PUT2, PUT1,PUT0)(__VA_ARGS__)

如果愿意,您可以将此解决方案扩展到任意多个参数。

实施

上面的宏调用了函数put_impl,它是如何打印n变体数组的实现。经过上面所有的技巧,函数就相当简单了:

void put_impl(size_t n, const var_t var[])
{
    for (size_t i = 0; i < n; i++) {
        switch(var[i].type) {
        case 'i':   printf("%i", var[i].data.i); break;
        case 'f':   printf("%g", var[i].data.f); break;
        case 's':   printf("%s", var[i].data.s); break;
        case '#':   printf("[undef]"); break;
        }
    }

    putchar('\n');
}

综合起来

以下程序使用上述方法打印一些相当愚蠢的东西。它不可移植,但如果使用 gcc -std=gnu99 编译则运行:

#include <stdlib.h>
#include <stdio.h>

#define CHOOSE __builtin_choose_expr
#define IFTYPE(X, T) __builtin_types_compatible_p(typeof(X), T)

#define VAR(X)                                          \
    CHOOSE(IFTYPE(X, int),          make_var_i,         \
    CHOOSE(IFTYPE(X, const char[]), make_var_s,         \
    CHOOSE(IFTYPE(X, const char *), make_var_s,         \
    CHOOSE(IFTYPE(X, double),       make_var_f,         \
                                    make_var_0))))(X)

#define PUT0()              put_impl(0, NULL)
#define PUT1(_1)            put_impl(1, (var_t[]){VAR(_1)})
#define PUT2(_1, _2)        put_impl(2, (var_t[]){VAR(_1), VAR(_2)})
#define PUT3(_1, _2, _3)    put_impl(3, (var_t[]){VAR(_1), VAR(_2), VAR(_3)})

#define SELECT_N(X, _1, _2, _3, N, ...) N

#define put(...) SELECT_N(X, ##__VA_ARGS__, PUT3, PUT2, PUT1,PUT0)(__VA_ARGS__)

typedef struct var_t var_t;

struct var_t {
    int type;
    union {
        int i;
        double f;
        const char *s;
    } data;
};

var_t make_var_i(int X)         { var_t v = {'i', {.i = X}}; return v; }
var_t make_var_s(const char *X) { var_t v = {'s', {.s = X}}; return v; }
var_t make_var_f(double X)      { var_t v = {'f', {.f = X}}; return v; }
var_t make_var_0()              { var_t v = {'#'}; return v; }

void put_impl(size_t n, const var_t var[])
{
    for (size_t i = 0; i < n; i++) {
        switch(var[i].type) {
        case 'i':   printf("%i", var[i].data.i); break;
        case 'f':   printf("%g", var[i].data.f); break;
        case 's':   printf("%s", var[i].data.s); break;
        case '#':   printf("[undef]"); break;
        }
    }

    putchar('\n');
}

int main()
{
    const char *name = "Fred";
    double C = 12.5;

    put(1, " ", 2);
    put("Hello, I'm ", name, "!");
    put();
    put(C, " Celsius");
    put(C * 1.8 + 32.0, " Fahrenheit");

    return 0;
}

您可以对要支持的参数类型和数量着迷,但请记住,您的宏丛林越大,维护和调试就越困难。