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

时间:2016-08-24 19:06:04

标签: c++ language-lawyer variadic-functions

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

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

此外,这些必须是cdecl函数,因此调用者可以负责,例如在复原时复制它们并在返回时摧毁它们。

任何见解都将受到赞赏。

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

3 个答案:

答案 0 :(得分: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传递,那显然不会起作用。

编辑:将对象传递给具有可变参数的函数的另一个问题是,在当前形式中,无论调用约定如何,“正确的东西”都需要两个副本,而正常的参数传递只需要一个。除非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);
}

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

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

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

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

答案 1 :(得分:9)

我正在记录这个,因为它太大而不能作为评论,并且花费这个时间非常耗时,所以没有人浪费时间往下看这条路线。

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

经过一番努力,我能够将措辞追溯到DR506

  

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

参考文献(N1727)对该主题的说法很少:

  

现有的措辞(5.2.2¶7)使得将非POD对象传递给函数调用中的省略号是未定义的行为:

     

{剪断}

     

再一次,CWG没有理由不要求实施在这种情况下发布诊断。

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

答案 2 :(得分: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);

修改

现在已删除的评论指出,上例中的ABD实际上是POD类型。但是,我提请你注意的问题与继承有关,虽然允许POD类型,但在大多数情况下涉及非POD类型。