首页 文章

为什么规范禁止将类类型传递给变量参数C函数?

提问于
浏览
24

将非POD传递给变量参数函数(如printf)是未定义的行为(12),但我不明白为什么C标准是这样设置的 . 变量arg函数中是否存在任何固有的东西阻止它们接受类作为参数?

变量arg callee确实对它们的类型一无所知 - 但它也不知道它接受的内置类型或普通POD .

而且,这些必须是cdecl函数,因此调用者可以负责,例如通过时复制它们并在返回时摧毁它们 .

任何见解将不胜感激 .


编辑:我仍然没有看到为什么建议的可变参数语义不起作用,但zneak的答案很好地说明了将编译器调整到它需要什么 - 所以我接受了它 . 最终,这可能是一些历史故障 .

3 回答

  • 12

    调用约定确实指定谁进行低级别堆栈跳舞,但它没有说明谁负责“高级”C簿记 . 至少在Windows上,按值接受对象的函数负责调用其析构函数,即使它不负责存储空间 . 例如,如果你构建这个:

    #include <stdio.h>
    
    struct Foo {
        Foo() { puts("created"); }
        Foo(const Foo&) { puts("copied"); }
        ~Foo() { puts("destroyed"); }
    };
    
    void __cdecl x(Foo f) { }
    
    int main() {
        Foo f;
        x(f);
        return 0;
    }
    

    你得到:

    x:
        mov     qword ptr [rsp+8],rcx
        sub     rsp,28h
        mov     rcx,qword ptr [rsp+30h]
        call    module!Foo::~Foo (00000001`400027e0)
        add     rsp,28h
        ret
    
    main:
        sub     rsp,48h
        mov     qword ptr [rsp+38h],0FFFFFFFFFFFFFFFEh
        lea     rcx,[rsp+20h]
        call    module!Foo::Foo (00000001`400027b0) # default ctor
        nop
        lea     rax,[rsp+21h]
        mov     qword ptr [rsp+28h],rax
        lea     rdx,[rsp+20h]
        mov     rcx,qword ptr [rsp+28h]
        call    module!Foo::Foo (00000001`40002780) # copy ctor
        mov     qword ptr [rsp+30h],rax
        mov     rcx,qword ptr [rsp+30h]
        call    module!x (00000001`40002810)
        mov     dword ptr [rsp+24h],0
        lea     rcx,[rsp+20h]
        call    module!Foo::~Foo (00000001`400027e0)
        mov     eax,dword ptr [rsp+24h]
        add     rsp,48h
        ret
    

    注意 main 如何构造两个 Foo 对象,但只销毁一个; x 照顾另一个 . 如果对象作为vararg传递,那显然不会起作用 .


    编辑:将对象传递给具有可变参数的函数的另一个问题是,在当前形式中,无论调用约定如何,"right thing"都需要两个副本,而正常参数传递只需要一个副本 . 除非C扩展C变量函数,因为它可以传递和/或接受对象的引用(这种情况极不可能发生,因为C使用可变参数模板以类型安全的方式解决了同样的问题),调用者需要制作该对象的一个副本, va_arg 仅允许被调用者获取该副本的副本 .

    微软的CL尝试在 va_arg 网站上使用一个按位拷贝和一个完整拷贝构造的那个按位拷贝,但它可能会产生令人讨厌的后果 . 考虑这个例子:

    struct foo {
        char* ptr;
    
        foo(const char* ptr) { this->ptr = _strdup(ptr); }
        foo(const foo& that) { ptr = _strdup(that.ptr); }
        ~foo() { free(ptr); }
    
        void setPtr(const char* ptr) {
            free(this->ptr);
            this->ptr = _strdup(ptr);
        }
    };
    
    void variadic(foo& a, ...)
    {
        a.setPtr("bar");
    
        va_list list;
        va_start(list, a);
        foo b = va_arg(list, foo);
        va_end(list);
    
        printf("%s %s\n", a.ptr, b.ptr);
    }
    
    int main() {
        foo f = "foo";
        variadic(f, f);
    }
    

    在我的机器上,这打印"bar bar",即使它打印"foo bar"如果我有一个非可变参数函数,其第二个参数接受另一个 foo 副本 . 这是因为 f 的按位副本发生在 variadic 的调用站点的 main 中,但只有在调用 va_arg 时才会调用复制构造函数 . 在两者之间, a.setPtr 使原始的 f.ptr 值无效,但是仍然存在于按位副本中,并且纯粹的巧合 _strdup 返回相同的指针(尽管内部有一个新的字符串) . 同一代码的另一个结果可能是 _strdup 崩溃 .

    请注意,此设计适用于POD类型;当构造函数和析构函数需要副作用时,它才会崩溃 .

    调用约定和参数传递机制不一定支持非平凡构造和对象破坏的原始观点仍然存在:这正是这里发生的事情 .


    编辑:答案最初说,建设和破坏行为是cdecl特有的;它不是 . (谢谢科迪!)

  • 9

    I'm recording this, because it's too big to be a comment, and it was reasonably time consuming to hunt this down, so no one else wastes time looking down this route.

    该文本首先改为类似于2006-11-03发布的标准草案中的当前措辞 .

    通过一些努力,我能够追溯到DR506的措辞 .

    论文J16 / 04-0167 = WG21 N1727建议将非POD对象传递给省略号是不正确的 . 然而,在利勒哈默尔 Session 的讨论中,CWG认为新批准的有条件支持行为类别更为合适 .

    引用的论文(N1727)对该主题的说法很少:

    现有的措辞(5.2.2¶7)使得将非POD对象传递给函数调用中的省略号是未定义的行为:再次,CWG没有理由不要求实现发出诊断这种情况 .

    然而,这并没有告诉我为什么它是这样的开始,这是你想知道的 . 我不可能将时钟转回到第一次写这种语言的时候,因为最早的免费提供的草案标准是从2005年起已经有了措辞你想知道,在此之前的所有标准要么需要认证,要么只是无内容 .

  • 6

    我想问题是/是否违反了类型安全 . 通常,将派生类对象传递给期望基类对象应该是安全的 . 如果基类对象是按值获取的,那么派生类对象将被简单地切片 . 如果它是由指针/引用 - 在编译期间正确调整派生类对象的指针/引用 . 这不适用于变量参数函数,其中输入类型的解释由代码而不是编译器执行 .

    例:

    struct A { char c; };
    struct B { int i; };
    struct D : A, B { double d; };
    
    // This is similar to printf, but also handles the
    // format specifier %b assuming an object of type B
    void non_pod_printf(const char* fmt, ...);
    
    D d1, d2;
    
    // I bet that the code inside non_pod_printf will fail to correctly
    // handle the d1 and d2 arguments even though the language rules
    // ensure that D is a B
    non_pod_printf("%d %b %b", 123, d1, d2);
    

    EDIT

    正如现在删除的注释所指出的,上例中的 ABD 实际上是POD类型 . 但是,我引起注意的问题与继承有关,虽然允许POD类型,但在大多数情况下涉及非POD类型 .

相关问题