Ответ 1
В вызывающем соглашении указывается, кто танцевать на низком уровне, но он не говорит, кто несет ответственность за бухгалтерию на высоком уровне. По крайней мере, в 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.
EDIT: Еще одна проблема с передачей объектов функциям с переменными параметрами заключается в том, что в его текущей форме, независимо от соглашения о вызове, "правильная вещь" требует двух копий, тогда как для нормальной передачи параметров требуется только одна. Если С++ не расширяет возможности C variadic, позволяя передавать и/или принимать ссылки на объекты (что крайне маловероятно когда-либо, учитывая, что С++ решает одну и ту же проблему безопасным способом с использованием вариативных шаблонов), вызывающий должен сделайте одну копию объекта, а 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 bar", если бы у меня была невариантная функция, второй параметр которой принял другую Foo
путем копирования. Это связано с тем, что побитная копия f
происходит в main
на сайте вызова variadic
, но конструктор копирования вызывается только при вызове va_arg
. Между двумя, a.setPtr
отменяет исходное значение f.ptr
, которое все же присутствует в побитовой копии, и по чистому совпадению _strdup
возвращает тот же указатель (хотя и с новой строкой внутри). Другим результатом того же кода может быть сбой в _strdup
.
Обратите внимание, что этот дизайн отлично подходит для типов POD; он разрушается только тогда, когда конструкторы и деструкторы нуждаются в побочных эффектах.
Исходная точка, заключающаяся в том, что вызовы условностей и механизмы передачи параметров не обязательно поддерживают нетривиальную конструкцию и уничтожение объектов, все еще стоит: это именно то, что происходит здесь.
EDIT: ответ первоначально сказал, что поведение конструкции и разрушения было специфичным для cdecl; это не. (Спасибо, Коди!)