【问题标题】:Function overloading in C using GCC - functions with mutiple arguments使用 GCC 在 C 中重载函数 - 具有多个参数的函数
【发布时间】:2016-04-11 14:11:48
【问题描述】:

在上一个问题中,我找到了一种在 C99 中重载函数的方法,当每个函数只接受一个参数时。详情请参阅:Function overloading in C using GCC - compiler warnings 中的答案。

现在我已经找到了一种使用单参数函数的方法,我想知道如何为接受多个参数的函数完成此操作。我认为它与__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被重新定义:

#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 关键字不可用。

【问题讨论】:

  • 稍微更正:您找到了一种使用 GCC extensions 模拟函数重载的方法。这绝对不是 C99 标准的一部分。
  • 对我来说糟糕的解决方案..顺便说一句,如果重载是强制性的,你不切换到 g++ 吗?
  • 我从未说过这是 c99 标准的一部分,事实上我在链接的问题中指定我使用的是 GCC。另外,这不是关于为什么应该或不应该这样做的问题,而是如何做的问题。应该这样做只是因为它可以做到。
  • @Olaf,看看标签。它被标记为 gcc。并且仅仅因为标题没有明确说明这不会使其产生误导。
  • 我也知道这是一种黑客行为,可能是一种糟糕的做法。这不是问题的一部分。我只是想知道它是否可以完成。不管该不该用,拥有它仍然是有用的知识。

标签: c gcc c99 overloading preprocessor-directive


【解决方案1】:

您可以使用 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 将产生灾难性的结果。

相反,您可以使用复合文字来动态创建doubles 数组。使用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 扩展 , ##__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;
}

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

【讨论】:

  • 这是令人难以置信的信息!我可能需要一段时间才能理解,但这太棒了!谢谢!
  • 如何让它识别不带参数的重载?例如:put();
  • @tjwrona1992:我通过 GCC 的 , ##__VA_ARGS__ 语法添加了对空参数列表的支持,如果可变参数不为空,它将在可变参数之前放置一个 cmma。
  • 我理解可导出函数不能支持重载的充分理由。考虑到编译器不应该为静态函数提供任何特定的链接时名称(并且在许多情况下必须修改名称以避免链接- 如果在不同编译单元中声明的静态函数具有相同的名称,则会出现错误)。
  • @supercat:静态函数可能会被重载,是的,但是重载函数作为与其他对象和库的通用接口更有意义,必须导出符号名称。
【解决方案2】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-28
    • 1970-01-01
    相关资源
    最近更新 更多