调用约定确实指定了谁进行低级堆栈舞蹈,但没有说明谁负责“高级”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 只允许被调用者获取该副本的副本。
Microsoft 的 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,它也会打印“foo bar”。这是因为f 的按位复制发生在main 中variadic 的调用点,但只有在调用va_arg 时才会调用复制构造函数。在两者之间,a.setPtr 使原始的f.ptr 值无效,但该值仍然存在于按位副本中,并且纯巧合_strdup 返回相同的指针(尽管其中包含一个新字符串)。相同代码的另一个结果可能是_strdup 中的崩溃。
请注意,这种设计非常适合 POD 类型;只有当构造函数和析构函数需要副作用时,它才会崩溃。
调用约定和参数传递机制不一定支持对象的非平凡构造和销毁的原始观点仍然存在:这正是这里发生的事情。
编辑:答案最初说构造和破坏行为是特定于 cdecl 的;它不是。 (谢谢科迪!)