Подтвердить что ты не робот

Почему спецификация запрещает передавать типы классов в функции С++ с переменными аргументами?

Передача не-PODs в переменные функции аргументов, такие как printf, - это поведение undefined (1, 2), но я не понимаю, почему стандарт С++ был установлен таким образом. Есть ли что-то, что присуще переменным функциям arg, которое мешает им принимать классы в качестве аргументов?

Вызывающая переменная-arg действительно ничего не знает о своем типе - но и не знает ничего о встроенных типах или простых POD, которые она принимает.

Кроме того, они обязательно являются функциями cdecl, поэтому вызывающий может быть ответственным, например. для копирования их при прохождении и уничтожении их по возвращении.

Любое понимание будет оценено.


EDIT: я по-прежнему не вижу причин, по которым предлагаемая вариационная семантика не будет работать, но ответ zneak хорошо показывает, что потребуется, чтобы настроить компиляторы на нее, - поэтому я принял ее. В конечном счете, это может быть некоторый исторический сбой.

4b9b3361

Ответ 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; это не. (Спасибо, Коди!)

Ответ 2

Я записываю это, потому что он слишком велик, чтобы быть комментарием, и было достаточно много времени, чтобы охотиться на это, так что никто больше не тратит время на поиск этого маршрута.

Текст был сначала изменен на нечто подобное текущей редакции в проекте стандарта в N2134, выпущенном в 2006-11-03.

С некоторыми усилиями я смог отследить формулировку DR506.

Бумага J16/04-0167 = WG21 N1727 предполагает, что передача объекта не-POD в эллипсис будет плохо сформирована. Вместе с тем в ходе дискуссий на совещании в Лиллехаммере КСР считала, что вновь утвержденная категория условно-поддерживаемого поведения будет более уместной.

Документ, на который делается ссылка (N1727), очень мало говорит по этому вопросу:

Существующая формулировка (5.2.2¶7) делает поведение undefined для передачи объекта не-POD в многоточие при вызове функции:

{Снип}

Еще раз, CWG не видит причин не требовать реализации для выдачи диагностики в таких случаях.

Однако, это не говорит мне очень много о том, почему именно так оно и должно было начаться, и это то, что вы хотите знать. Возвращение часов назад к тому, когда этот язык был впервые написан, для меня невозможен, потому что самый старый свободно доступный черновик с 2005 года и уже имеет формулировку, о которой вам интересно, все стандарты до этого требуют либо аутентификации, либо просто бессодержательная.

Ответ 3

Я предполагаю, что проблема заключается в нарушении безопасности типа. Как правило, передача объекта производного класса, где ожидается объект базового класса, должна быть безопасной. Если объект базового класса принимается по значению, то объект производного класса будет просто нарезанным. Если он берется указателем/ссылкой - указатель/ссылка на объект производного класса правильно корректируется во время компиляции. Это не работает с переменными аргументами, где интерпретация типов ввода выполняется кодом, а не компилятором.

Пример:

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-типы.