【问题标题】:Why this method for function overloading in C works?为什么这种在 C 中用于函数重载的方法有效?
【发布时间】:2018-02-24 23:54:33
【问题描述】:

我已经查看了一些在 C 中执行此操作的方法,但我只找到了 C99。

但我遇到了以下解决方案,取自Lock Less

问题是,我不太了解它是如何工作的,我想知道那里发生的事情的基本原理以便能够更清楚地理解它。

我在网上搜索了一段时间,发现 this 关于 __VA_ARGS__,但不幸的是,仅此还不够。

我非常感谢您对此问题的解释或一些指导,任何类型的参考都会有所帮助。

我已经使用带有 -ansi 标志的 GCC-5.4.1 编译了这段代码。

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

#define COUNT_PARMS2(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _, ...) _
#define COUNT_PARMS(...)\
    COUNT_PARMS2(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

void count_overload1(int p1)
{
    printf("One param: %d\n", p1);
}

void count_overload2(double *p1, const char *p2)
{
    printf("Two params: %p (%f) %s\n", p1, *p1, p2);
}

void count_overload3(int p1, int p2, int p3)
{
    printf("Three params: %c %d %d\n", p1, p2, p3);
}

void count_overload_aux(int count, ...)
{
    va_list v;
    va_start(v, count);

    switch(count)
    {
        case 1:
        {
            int p1 = va_arg(v, int);
            count_overload1(p1);
            break;
        }

        case 2:
        {
            double *p1 = va_arg(v, double *);
            const char *p2 = va_arg(v, const char *);
            count_overload2(p1, p2);
            break;
        }

        case 3:
        {
            int p1 = va_arg(v, int);
            int p2 = va_arg(v, int);
            int p3 = va_arg(v, int);
            count_overload3(p1, p2, p3);
            break;
        }

        default:
        {
            va_end(v);

            printf("Invalid arguments to function 'count_overload()'");
            exit(1);
        }
    }

    va_end(v);
}
#define count_overload(...)\
    count_overload_aux(COUNT_PARMS(__VA_ARGS__), __VA_ARGS__)


int main(int argc, char const *argv[])
{
    double d = 3.14;
    count_overload(1);
    count_overload(&d, "test");
    count_overload('a',2,3);
    return 0;
}

输出是:

One param: 1
Two params: 0x7ffc0fbcdd30 (3.140000) test
Three params: a 2 3

【问题讨论】:

  • 这不是函数重载。这只是向编写源代码的开发人员模拟函数重载。
  • 这对于开发人员来说看起来就像count_overload 是一个可变参数函数。您只是模糊了被调用函数所需的 count 参数,以了解它何时必须处理额外的参数,而不是从其他给定参数中隐式地知道这一点。 C 中的其他函数,例如printf,通过格式字符串包含的内容确定要访问的参数数量和类型。 printf 看起来与count_overload 一样是“重载”函数。在这里的封面下,你没有超载任何东西。这只是可变参数。
  • 然后在count_overload_aux 中存在开销,然后才开始实际调用真正的函数。总而言之,这是你在真实代码中永远不应该做的事情。
  • @BernardoDuarte:这一切都很好,很花哨,除了这会严重减少甚至消除编译时类型检查。因此,除非所有相关人员都确切地知道宏替换和参数解释发生了什么,否则您可能会遇到真正的调试问题。
  • 如果你不关心编译时类型检查,那么只需使用类似 printf 的接口,其中第一个参数是描述以下参数的字符串。例如,foo("dpi", 3.0, &amp;x, 42),其中“格式”解释为 d 表示 double,p 表示指向 int 的指针,i 表示 int。对于开发人员来说,这比所有的宏废话更容易理解,它支持不同类型的等量参数,并且与宏解决方案相比既不安全也不安全。

标签: c overloading


【解决方案1】:

让我们分解COUNT_PARMSCOUNT_PARMS2 宏。先COUNT_PARMS

#define COUNT_PARMS(...)\
    COUNT_PARMS2(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

由于宏不包含命名参数,因此传递给它的任何参数都将代替__VA_ARGS__

所以下面的调用:

COUNT_PARMS(arg1)
COUNT_PARMS(arg1, arg2)
COUNT_PARMS(arg1, arg2, ,arg3)

将扩展为:

COUNT_PARMS2(arg1,   10,    9,  8, 7, 6, 5, 4, 3, 2, 1)
COUNT_PARMS2(arg1, arg2,   10,  9, 8, 7, 6, 5, 4, 3, 2, 1)
COUNT_PARMS2(arg1, arg2, arg3, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
                                                  // x

我将参数隔开,以便您可以看到哪些参数相互对应。请特别注意标记为x 的列。这是传递给COUNT_PARMS 的参数数量,在每种情况下都是第 11 个参数。

现在让我们看看COUNT_PARMS2

#define COUNT_PARMS2(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _, ...) _

有 11 个名称参数,加上 ... 以解释任何其他参数。宏的整个主体是_,这是第11 个参数的名称。所以这个宏的目的是接受 11 个或更多的参数,并用第 11 个参数替换它们。

再次查看COUNT_PARAMS 的定义,它扩展为调用COUNT_PARMS2,第11 个参数是传递给COUNT_PARAMS 的参数数量。魔法就是这样发生的。

现在查看main中的函数调用:

count_overload(1);
count_overload(&d, "test");
count_overload('a',2,3);

这些扩展为:

count_overload_aux(COUNT_PARMS(1), 1);
count_overload_aux(COUNT_PARMS(&d, "test"), &d, "test");
count_overload_aux(COUNT_PARMS('a',2,3), 'a',2,3);

然后这个:

count_overload_aux(COUNT_PARMS2(1, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1), 1);
count_overload_aux(COUNT_PARMS2(&d, "test", 10, 9, 8, 7, 6, 5, 4, 3, 2, 1), &d, "test");
count_overload_aux(COUNT_PARMS2('a',2,3, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1), 'a',2,3);

然后这个:

count_overload_aux(1, 1);
count_overload_aux(2, &d, "test");
count_overload_aux(3, 'a',2,3);

最终结果是,您可以调用一个接受可变数量参数的函数,而无需明确说明有多少。

【讨论】:

    【解决方案2】:

    dbush's great answer 解释了宏在做什么。我想 对此进行扩展并讨论此处使用的省略号...。你说 阅读可变参数宏和__VA_ARGS__ 并没有帮助,所以我 假设您也不太了解 C 省略号。

    在 C 中,声明一个接受可变数量参数的函数的方法是 使用省略号 ...。这种函数的一个典型例子是printf,它 可以接受至少一个参数,但它可以接受更多。

    printf的原型是:

    int printf(const char *format, ...);
    

    ... 用于声明省略号。注意... 只能 出现在命名参数的末尾,它不应该是一个寄存器 变量、函数或数组类型,因此:

    void foo(...)
    {
    }
    

    无效,编译器会显示如下错误:

    c.c:6:10: error: ISO C requires a named argument before ‘...’
     void foo(...)
              ^~~
    

    那么,如何使用它?你使用在stdarg.h中定义的va_list

    #include<stdio.h>
    #include<stdarg.h>
    
    int sum(int num_of_values, ...)
    {
        va_list ap;
    
        // use the last named argument
        va_start(ap, num_of_values);
    
        int s = 0;
        for(int i = 0; i < num_of_values; ++i)
        {
            int v = va_arg(ap, int);
            s += v;
        }
    
        va_end(ap);
    
        return s;
    }
    
    int main(void)
    {
        printf("The sum is: %d\n", sum(5, 1, 2, 3, 4, 5));
    }
    

    这将输出The sum is: 15

    所以当你的函数有省略号时,你必须先声明一个变量类型 va_list 并调用 va_start 将该变量作为第一个参数,然后 最后命名的参数作为第二个参数。

    然后您可以使用va_arg(ap, &lt;type&gt;) 获取值,其中&lt;type&gt; 是 值的类型,在上面的示例中,它将是int。像这样的功能 printf 解析格式并使用转换说明符得到正确的 类型。当printf 建立一个%d 时,它会做va_arg(ap, int),如果%f 是 发现它会做va_arg(ap, float),如果找到%s,它会做 va_arg(ap, char*) 等等。这就是为什么printf 在格式化时具有未定义的行为 并且参数不匹配,因为在 va_arg 调用与va_arg 的后续调用混淆。最后必须调用va_end

    对于我在大学期间必须编写的微内核,我有 实现这些va_*-宏。我使用了编译器的行为 将所有参数放在堆栈帧中,所以我的va_start 计算了地址 最后一个命名参数之后的下一个值的堆栈。 va_arg 在堆栈中移动 基于va_start 的计算加上由类型确定的偏移量 while 还使用最后使用的参数更新 ap 变量。它是 让它工作起来很棘手,但最后它在那个系统上工作了,但是同样的实现 在x86_64 上只会产生垃圾。

    例如,这是如何实现的 在 GCC 编译器中,我不知道,但我怀疑 GCC 做了什么 相似的。我检查了源代码gcc/builtins.c:4855,但像往常一样, 我发现 GCC 代码非常复杂。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多