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

Ofe*_*lon 24 c++ variadic-functions language-lawyer

通过非荚可变参数函数,如printf是未定义的行为(1,2),但我不理解为什么 C++标准设定这种方式.变量arg函数中是否存在任何固有的东西阻止它们接受类作为参数?

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

此外,这些必然是cdecl函数,因此调用者可以负责例如在传递时复制它们并在返回时销毁它们.

任何见解将不胜感激.


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

zne*_*eak 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;
}
Run Code Online (Sandbox Code Playgroud)

你得到:

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
Run Code Online (Sandbox Code Playgroud)

注意如何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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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


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

  • 这个答案中的术语有些误导.尽管Windows平台上存在`__cdecl`调用约定,但它仅适用于32位代码.您的目标代码示例显然是64位代码,因为它使用64位寄存器.Windows上的64位平台没有`__cdecl`.实际上,AMD64只有两种调用约定:(a)标准的Microsoft 64位调用约定,它没有名称,以及(b)`__ vectorcall`. (3认同)

Omn*_*ity 9

我正在记录这个,因为它太大而不能作为评论,并且花费这个时间相当耗时,所以没有人浪费时间俯视这条路线.

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

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

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

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

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

{}剪断

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

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


Leo*_*eon 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);
Run Code Online (Sandbox Code Playgroud)

编辑

作为现在删除评论中指出,A,BD在上面的例子中实际上是POD类型.但是,我引起注意的问题与继承有关,虽然允许POD类型,但在大多数情况下涉及非POD类型.