如何在C语言中实现函数重载?

307

在C语言中有没有实现函数重载的方法?我希望能够像其他语言一样对简单函数进行重载。

foo (int a)  
foo (char b)  
foo (float c , int d)

我认为没有直接的方法;如果有任何变通方法,我正在寻找。


9
为什么要这样做?C语言没有多态的能力,因此无法实现foo(random type)。只需创建真正的函数foo_i、foo_ch、foo_d等即可。 - jmucchiello
9
你可以使用空指针和类型标识来走邪恶的路线。 - alk
20
我觉得我应该提醒大家,由于新的 C 标准,这个问题的答案已经与最初提出时不同了。 - Alex Celeste
16个回答

325

是的!

自从提出这个问题以来,标准 C(没有扩展)已经通过在 C11 中添加 _Generic 关键字(在 GCC 版本 4.9 中得到支持)有效地获得了函数重载的支持(不是运算符)。

(重载并不真正地以问题中展示的方式“内置”,但很容易实现像那样工作的东西。)

_Generic 是一个编译时操作符,与 sizeof_Alignof 属于同一家族。它在标准第6.5.1.1节中进行了描述。它接受两个主要参数:一个表达式(不会在运行时计算),和一个类型/表达式关联列表,看起来有点像 switch 块。 _Generic 获取表达式的整体类型然后对其进行“切换”,以选择列表中其类型的最终结果表达式:

_Generic(1, float: 2.0,
            char *: "2",
            int: 2,
            default: get_two_object());

上述表达式求值为2 - 控制表达式的类型是int,因此它选择与int相关联的表达式作为值。这在运行时不会发生任何事情。(default子句是可选的:如果您将其省略并且类型不匹配,则会导致编译错误。)
这对于函数重载的有用之处在于它可以由C预处理器插入,并根据传递给控制宏的参数的类型选择结果表达式。所以(来自C标准的例子):
#define cbrt(X) _Generic((X),                \
                         long double: cbrtl, \
                         default: cbrt,      \
                         float: cbrtf        \
                         )(X)

此宏实现了一个重载的cbrt操作,通过根据宏参数的类型进行分发,选择适当的实现函数,然后将原始宏参数传递给该函数。
因此,要实现您的原始示例,我们可以这样做:
foo_int (int a)  
foo_char (char b)  
foo_float_int (float c , int d)

#define foo(_1, ...) _Generic((_1),                                  \
                              int: foo_int,                          \
                              char: foo_char,                        \
                              float: _Generic((FIRST(__VA_ARGS__,)), \
                                     int: foo_float_int))(_1, __VA_ARGS__)
#define FIRST(A, ...) A

在这种情况下,我们本可以对第三种情况使用“default:”关联,但这并没有展示如何将原则扩展到多重参数。最终结果是,您可以在代码中使用foo(...)而不太担心其参数的类型。

编辑 Cosinus 提供了一种更加优雅的解决方案,可以处理 C23 或 GNU 扩展中的多参数重载问题, 下面的技术是针对 C11 编写的(C11 实际上并不希望您这样做)。

对于更复杂的情况,例如函数重载较多参数或可变数量的情况,您可以使用实用宏来自动生成静态分派结构:

void print_ii(int a, int b) { printf("int, int\n"); }
void print_di(double a, int b) { printf("double, int\n"); }
void print_iii(int a, int b, int c) { printf("int, int, int\n"); }
void print_default(void) { printf("unknown arguments\n"); }

#define print(...) OVERLOAD(print, (__VA_ARGS__), \
    (print_ii, (int, int)), \
    (print_di, (double, int)), \
    (print_iii, (int, int, int)) \
)

#define OVERLOAD_ARG_TYPES (int, double)
#define OVERLOAD_FUNCTIONS (print)
#include "activate-overloads.h"

int main(void) {
    print(44, 47);   // prints "int, int"
    print(4.4, 47);  // prints "double, int"
    print(1, 2, 3);  // prints "int, int, int"
    print("");       // prints "unknown arguments"
}

(在这里实现) 因此,通过一些努力,您可以将大量样板代码减少到几乎看起来像具有本地支持重载的语言。

另外,在C99中已经可以根据参数数量(而不是类型)进行重载。


[1] 注意,C语言评估类型的方式可能会让你感到困惑。例如,如果你尝试传递一个字符字面量,它将选择foo_int,但如果你想要重载支持字符串字面量,则需要进行一些调整。总体而言还是非常酷的。


1
根据您的示例,似乎唯一被重载的是函数宏。让我看看是否理解正确:如果要重载函数,您只需使用预处理器根据传递的数据类型重定向函数调用,对吗? - Nick
12
@Nick,这就是重载的全部内容。在其他语言中,它只是以隐式方式处理(例如,在任何语言中都无法真正获得“指向重载函数的指针”,因为重载意味着多个体)。请注意,这不能仅通过预处理器完成,它需要某种类型的类型分派;预处理器只会更改外观。 - Alex Celeste
5
作为一个相对熟悉C99并想学习如何做到这一点的人,这似乎过于复杂,即使对于C语言来说也是如此。 - Tyler Crompton
7
它是在编译时进行评估的。 - JAB
1
我发现了一种更好地利用_Generic()的方法。我利用函数指针可以被定义多次且仍然相同的事实。通过这个,我可以将任何参数列表组合成一种类型。请参见我的答案。 - Cosinus
显示剩余5条评论

141
有几种可能性:
1. 使用类似于printf的函数(将类型作为参数); 2. 使用类似于OpenGL的函数(在函数名中指定类型); 3. 使用C++的子集(如果您可以使用C++编译器)。

1
你能解释或提供OpenGL风格功能的链接吗? - FL4SOF
1
@Lazer:这里有一个简单的类似printf函数的实现。 - Alexey Frunze
21
不,printf不是函数重载。它使用可变参数!而且C不支持函数重载。 - hqt
77
回答中从未提到“重载”这个词。 - remmy
3
@kyrias 如果答案与重载无关,那么它就不是正确的问题。 - Michael Mrozek
显示剩余3条评论

88

正如已经提到的,C语言不支持您所说的重载。解决此问题的常见习惯用法是使函数接受一个标记联合体。这可以通过使用struct参数实现,其中struct本身由某种类型指示器(例如enum)和不同类型值的union组成。例如:

#include <stdio.h>

typedef enum {
    T_INT,
    T_FLOAT,
    T_CHAR,
} my_type;

typedef struct {
    my_type type;
    union {
        int a; 
        float b; 
        char c;
    } my_union;
} my_struct;

void set_overload (my_struct *whatever) 
{
    switch (whatever->type) 
    {
        case T_INT:
            whatever->my_union.a = 1;
            break;
        case T_FLOAT:
            whatever->my_union.b = 2.0;
            break;
        case T_CHAR:
            whatever->my_union.c = '3';
    }
}

void printf_overload (my_struct *whatever) {
    switch (whatever->type) 
    {
        case T_INT:
            printf("%d\n", whatever->my_union.a);
            break;
        case T_FLOAT:
            printf("%f\n", whatever->my_union.b);
            break;
        case T_CHAR:
            printf("%c\n", whatever->my_union.c);
            break;
    }

}

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

    s.type=T_INT;
    set_overload(&s);
    printf_overload(&s);

    s.type=T_FLOAT;
    set_overload(&s);
    printf_overload(&s);

    s.type=T_CHAR;
    set_overload(&s);
    printf_overload(&s); 
}

24
为什么不把所有的whatever都变成单独的函数(如set_intset_float等)?然后“标记类型”就变成了“将类型名称添加到函数名称中”。这个答案中的版本涉及更多的打字、更多的运行时成本、更多的错误可能无法在编译时捕获……我完全看不出以这种方式做事情有任何优势!16个赞?! - Ben
32
这个答案被点赞是因为它“回答了问题”,而不是只是说“不要那样做”。你是正确的,在C语言中,使用单独的函数更符合惯用语法,但如果想在C中使用多态性,这是一个好的方法。此外,这个答案展示了如何在编译器或虚拟机中实现运行时多态性:用类型标记值,然后根据该标记进行分发。因此,它是对原始问题的出色回答。 - Nils von Barth
@NilsvonBarth 但问题是关于重载,而不是多态性,使用后者来模拟前者是不好的建议。 - Dmitry Grigoryev
很好的回答,但不幸的是它没有涵盖函数模拟需要接受多个参数的情况,因为union只为其最大成员分配空间。我可以看到你可以通过定义一个struct来解决这个问题,以存储相关变量,但我不确定这是否会引发一些潜在问题。 - Mehdi Charife
很好的回答,但不幸的是它没有涵盖函数模拟需要接受多个参数的情况,因为union只为其最大成员分配空间。我可以看到你可以通过定义一个struct来解决这个问题,以存储相关变量,但我不确定这是否会引发一些潜在问题。 - undefined

35

下面是我找到的最清晰、最简洁的C语言函数重载示例:

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

int addi(int a, int b) {
    return a + b;
}

char *adds(char *a, char *b) {
    char *res = malloc(strlen(a) + strlen(b) + 1);
    strcpy(res, a);
    strcat(res, b);
    return res;
}

#define add(a, b) _Generic(a, int: addi, char*: adds)(a, b)

int main(void) {
    int a = 1, b = 2;
    printf("%d\n", add(a, b)); // 3

    char *c = "hello ", *d = "world";
    printf("%s\n", add(c, d)); // hello world

    return 0;
}

https://gist.github.com/barosl/e0af4a92b2b8cabd05a7


3
我认为这篇内容在精神上与 https://dev59.com/u3RB5IYBdhLWcg3w4bGv#25026358 重复(但解释较少)。 - Andy Hayden
3
我肯定更喜欢一个完整可运行的代码块,而不是像#1240268那样割裂开来的代码。人各有所好。 - Jay Taylor
3
我更喜欢那些解释其做法和原理的答案。这个回答两者都没有。“最好的我见过的”并不是阐述。 - underscore_d
1
此示例存在内存泄漏,请查看提供的 gist 上的注释:"您通过在打印拼接字符串之前未存储指向该字符串的指针来创建内存泄漏,因为现在您无法在使用 malloc 分配内存后释放它。" - gresolio

19

如果您使用的编译器是gcc,并且每次添加新的重载时都不介意手动更新,那么您可以使用一些宏技巧来获取所需的调用者结果,尽管写起来不太方便...但这是可能的。

查看__builtin_types_compatible_p,然后使用它来定义一个类似于以下内容的宏:

#define foo(a) \
((__builtin_types_compatible_p(int, a)?foo(a):(__builtin_types_compatible_p(float, a)?foo(a):)

但是,确实很恶心,不要这样做。

编辑: C1X将支持类型通用表达式,它们看起来像这样:

#define cbrt(X) _Generic((X), long double: cbrtl, \
                              default: cbrt, \
                              float: cbrtf)(X)

15

这可能并没有帮助,但如果你正在使用clang,你可以使用可重载属性 - 这在作为C编译时也有效。

http://clang.llvm.org/docs/AttributeReference.html#overloadable

Header

extern void DecodeImageNow(CGImageRef image, CGContextRef usingContext) __attribute__((overloadable));
extern void DecodeImageNow(CGImageRef image) __attribute__((overloadable));

实现

void __attribute__((overloadable)) DecodeImageNow(CGImageRef image, CGContextRef usingContext { ... }
void __attribute__((overloadable)) DecodeImageNow(CGImageRef image) { ... }

哇。我尝试使用其他评论中描述的_Generic,现在我认为它太复杂了,不切实际 - 放弃函数重载的整个想法更容易。这个解决方案要简单得多,也是clang的一个很好的论据。谢谢。 - makingthematrix

13
是的,有点类似。以下是一个例子:

这里提供一个示例:

void printA(int a){
printf("Hello world from printA : %d\n",a);
}

void printB(const char *buff){
printf("Hello world from printB : %s\n",buff);
}

#define Max_ITEMS() 6, 5, 4, 3, 2, 1, 0 
#define __VA_ARG_N(_1, _2, _3, _4, _5, _6, N, ...) N
#define _Num_ARGS_(...) __VA_ARG_N(__VA_ARGS__) 
#define NUM_ARGS(...) (_Num_ARGS_(_0, ## __VA_ARGS__, Max_ITEMS()) - 1) 
#define CHECK_ARGS_MAX_LIMIT(t) if(NUM_ARGS(args)>t)
#define CHECK_ARGS_MIN_LIMIT(t) if(NUM_ARGS(args) 
#define print(x , args ...) \
CHECK_ARGS_MIN_LIMIT(1) printf("error");fflush(stdout); \
CHECK_ARGS_MAX_LIMIT(4) printf("error");fflush(stdout); \
({ \
if (__builtin_types_compatible_p (typeof (x), int)) \
printA(x, ##args); \
else \
printB (x,##args); \
})

int main(int argc, char** argv) {
    int a=0;
    print(a);
    print("hello");
    return (EXIT_SUCCESS);
}

它将输出0和hello..来自printA和printB。


2
int main(int argc, char** argv) { int a=0; print(a); print("hello"); return (EXIT_SUCCESS); }将从printA和printB输出0和hello... - Captain Barbossa
1
__builtin_types_compatible_p,这不是GCC编译器特有的吗? - Sogartar

13

下面的方法类似于 a2800276 的方法,但添加了一些 C99 宏魔法:

// we need `size_t`
#include <stddef.h>

// argument types to accept
enum sum_arg_types { SUM_LONG, SUM_ULONG, SUM_DOUBLE };

// a structure to hold an argument
struct sum_arg
{
    enum sum_arg_types type;
    union
    {
        long as_long;
        unsigned long as_ulong;
        double as_double;
    } value;
};

// determine an array's size
#define count(ARRAY) ((sizeof (ARRAY))/(sizeof *(ARRAY)))

// this is how our function will be called
#define sum(...) _sum(count(sum_args(__VA_ARGS__)), sum_args(__VA_ARGS__))

// create an array of `struct sum_arg`
#define sum_args(...) ((struct sum_arg []){ __VA_ARGS__ })

// create initializers for the arguments
#define sum_long(VALUE) { SUM_LONG, { .as_long = (VALUE) } }
#define sum_ulong(VALUE) { SUM_ULONG, { .as_ulong = (VALUE) } }
#define sum_double(VALUE) { SUM_DOUBLE, { .as_double = (VALUE) } }

// our polymorphic function
long double _sum(size_t count, struct sum_arg * args)
{
    long double value = 0;

    for(size_t i = 0; i < count; ++i)
    {
        switch(args[i].type)
        {
            case SUM_LONG:
            value += args[i].value.as_long;
            break;

            case SUM_ULONG:
            value += args[i].value.as_ulong;
            break;

            case SUM_DOUBLE:
            value += args[i].value.as_double;
            break;
        }
    }

    return value;
}

// let's see if it works

#include <stdio.h>

int main()
{
    unsigned long foo = -1;
    long double value = sum(sum_long(42), sum_ulong(foo), sum_double(1e10));
    printf("%Le\n", value);
    return 0;
}

12

就你的意思而言,不行。

你可以像这样声明一个va_arg函数:

void my_func(char* format, ...);

但是你需要在第一个参数中传递一些有关变量数量和类型的信息,就像printf()一样。


6

Leushenko的回答非常棒 - 仅有一个foo的例子在GCC下无法编译,它在foo(7)处失败,跌倒在FIRST宏和实际函数调用((_1,__VA_ARGS__))上,留下了多余的逗号。此外,如果我们想提供其他重载,比如foo(double),我们就会遇到麻烦。

因此,我决定进一步阐述这个答案,包括允许一个空重载(foo(void) - 这引起了很多麻烦...)。

现在的想法是:在不同的宏中定义多个通用程序,并根据参数数量选择正确的程序!

参数数量很容易,基于这个答案

#define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__)

#define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__)
#define CONCAT(X, Y) CONCAT_(X, Y)
#define CONCAT_(X, Y) X ## Y

很好,我们决定要么选择SELECT_1要么选择SELECT_2(或者更多参数,如果你想/需要它们),因此我们只需要适当的定义:

#define SELECT_0() foo_void
#define SELECT_1(_1) _Generic ((_1),    \
        int: foo_int,                   \
        char: foo_char,                 \
        double: foo_double              \
)
#define SELECT_2(_1, _2) _Generic((_1), \
        double: _Generic((_2),          \
                int: foo_double_int     \
        )                               \
)

好的,我已经添加了void重载 - 但是,这个实际上不符合C标准,因为它不允许空的可变参数,也就是说我们依赖于编译器扩展

首先,一个空的宏调用(foo())仍然会产生一个标记,但是是一个空的标记。所以即使在空的宏调用时,计数宏实际上返回1而不是0。如果列表为空或不为空,我们可以有条件地__VA_ARGS__后面放置逗号来"轻松"解决这个问题:

#define NARG(...) ARG4_(__VA_ARGS__ COMMA(__VA_ARGS__) 4, 3, 2, 1, 0)

那看起来很容易,但是“COMMA”宏非常重;幸运的是,这个主题已经在Jens Gustedt的博客中得到了涵盖(感谢Jens)。基本技巧是,如果没有跟随括号,则不会展开函数宏,有关详细说明,请查看Jens的博客...我们只需要稍微修改一下宏以满足我们的需求(为了简洁起见,我将使用更短的名称和较少的参数)。
#define ARGN(...) ARGN_(__VA_ARGS__)
#define ARGN_(_0, _1, _2, _3, N, ...) N
#define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 1, 0)

#define SET_COMMA(...) ,

#define COMMA(...) SELECT_COMMA             \
(                                           \
        HAS_COMMA(__VA_ARGS__),             \
        HAS_COMMA(__VA_ARGS__ ()),          \
        HAS_COMMA(SET_COMMA __VA_ARGS__),   \
        HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \
)

#define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3)
#define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3

#define COMMA_0000 ,
#define COMMA_0001
#define COMMA_0010 ,
// ... (all others with comma)
#define COMMA_1111 ,

现在我们很好...

完整的代码块:

/*
 * demo.c
 *
 *  Created on: 2017-09-14
 *      Author: sboehler
 */

#include <stdio.h>

void foo_void(void)
{
    puts("void");
}
void foo_int(int c)
{
    printf("int: %d\n", c);
}
void foo_char(char c)
{
    printf("char: %c\n", c);
}
void foo_double(double c)
{
    printf("double: %.2f\n", c);
}
void foo_double_int(double c, int d)
{
    printf("double: %.2f, int: %d\n", c, d);
}

#define foo(...) SELECT(__VA_ARGS__)(__VA_ARGS__)

#define SELECT(...) CONCAT(SELECT_, NARG(__VA_ARGS__))(__VA_ARGS__)
#define CONCAT(X, Y) CONCAT_(X, Y)
#define CONCAT_(X, Y) X ## Y

#define SELECT_0() foo_void
#define SELECT_1(_1) _Generic ((_1), \
        int: foo_int,                \
        char: foo_char,              \
        double: foo_double           \
)
#define SELECT_2(_1, _2) _Generic((_1), \
        double: _Generic((_2),          \
                int: foo_double_int     \
        )                               \
)

#define ARGN(...) ARGN_(__VA_ARGS__)
#define ARGN_(_0, _1, _2, N, ...) N

#define NARG(...) ARGN(__VA_ARGS__ COMMA(__VA_ARGS__) 3, 2, 1, 0)
#define HAS_COMMA(...) ARGN(__VA_ARGS__, 1, 1, 0)

#define SET_COMMA(...) ,

#define COMMA(...) SELECT_COMMA             \
(                                           \
        HAS_COMMA(__VA_ARGS__),             \
        HAS_COMMA(__VA_ARGS__ ()),          \
        HAS_COMMA(SET_COMMA __VA_ARGS__),   \
        HAS_COMMA(SET_COMMA __VA_ARGS__ ()) \
)

#define SELECT_COMMA(_0, _1, _2, _3) SELECT_COMMA_(_0, _1, _2, _3)
#define SELECT_COMMA_(_0, _1, _2, _3) COMMA_ ## _0 ## _1 ## _2 ## _3

#define COMMA_0000 ,
#define COMMA_0001
#define COMMA_0010 ,
#define COMMA_0011 ,
#define COMMA_0100 ,
#define COMMA_0101 ,
#define COMMA_0110 ,
#define COMMA_0111 ,
#define COMMA_1000 ,
#define COMMA_1001 ,
#define COMMA_1010 ,
#define COMMA_1011 ,
#define COMMA_1100 ,
#define COMMA_1101 ,
#define COMMA_1110 ,
#define COMMA_1111 ,

int main(int argc, char** argv)
{
    foo();
    foo(7);
    foo(10.12);
    foo(12.10, 7);
    foo((char)'s');

    return 0;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接