将非POD传递给变量参数函数(如printf)是未定义的行为(1,2),但我不明白为什么设置了C ++标准这条路。变量arg函数中是否存在任何固有的东西阻止它们将类作为参数接受?
变量arg callee确实对它们的类型一无所知 - 但它也不知道它所接受的内置类型或普通POD。
此外,这些必须是cdecl函数,因此调用者可以负责,例如在复原时复制它们并在返回时摧毁它们。
任何见解都将受到赞赏。
编辑:我仍然没有看到为什么建议的可变参数语义不起作用,但是zneak的答案很好地说明了将编译器调整到它所需要的 - 所以我接受了它。最终,这可能是一些历史故障。答案 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传递,那显然不会起作用。
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);
修改强>
现在已删除的评论指出,上例中的A
,B
和D
实际上是POD类型。但是,我提请你注意的问题与继承有关,虽然允许POD类型,但在大多数情况下涉及非POD类型。