c++ - todas - tipos de funciones en lenguaje c
¿Por qué la especificación prohíbe pasar tipos de clase a funciones de C++ de argumento variable? (3)
Pasar funciones que no son POD a argumentos variables como printf es un comportamiento indefinido ( 1 , 2 ), pero no entiendo por qué el estándar C ++ se estableció de esta manera. ¿Hay algo inherente en las funciones de la variable arg que les impida aceptar clases como argumentos?
El llamado de la variable-arg en realidad no sabe nada acerca de su tipo, pero tampoco sabe nada acerca de los tipos integrados o los POD simples que acepta.
Además, estas son necesariamente funciones cdecl, por lo que la persona que llama puede ser responsable, por ejemplo, de copiarlas al pasarlas y destruirlas al devolverlas.
Cualquier idea sería apreciada.
EDIT: Todavía no veo ninguna razón por la cual la semántica variad sugerida no funcione, pero la respuesta de zneak demuestra bien lo que se necesitaría para ajustar los compiladores a ella, así que lo acepté. En última instancia, podría ser algún fallo histórico.
La convención de llamada especifica quién hace el baile de pila de bajo nivel, pero no dice quién es responsable de la contabilidad de alto nivel en C ++. Al menos en Windows, una función que acepta un objeto por valor es responsable de llamar a su destructor, aunque no sea responsable del espacio de almacenamiento. Por ejemplo, si construyes esto:
#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;
}
usted obtiene:
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
Observe cómo main
construye dos objetos Foo
pero destruye solo uno; x
cuida del otro. Eso obviamente no funcionaría si el objeto se pasara como vararg.
EDITAR: Otro problema con el paso de objetos a funciones con parámetros variables es que en su forma actual, independientemente de la convención de llamada, lo "correcto" requiere dos copias, mientras que el paso normal de parámetros requiere solo una. A menos que C ++ extendiera las funciones variadicas de C al hacer posible pasar y / o aceptar referencias a objetos (lo cual es extremadamente improbable que ocurra, dado que C ++ resuelve el mismo problema de una manera segura con el uso de plantillas variadic), el llamador necesita haga una copia del objeto, y va_arg
solo le permite a la persona que llama obtener una copia de esa copia.
El CL de Microsoft intenta salirse con una copia bitwise y una copia completa de esa copia bitwise en el sitio va_arg
, pero puede tener consecuencias desagradables. Considera este ejemplo:
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);
}
En mi máquina, esto imprime "barra de barra", aunque se imprimiría "barra de foo" si tuviera una función no variada cuyo segundo parámetro aceptara otra copia de foo
. Esto se debe a que una copia bit a bit de f
ocurre en main
en el sitio de llamada de variadic
, pero el constructor de copia solo se invoca cuando se llama a va_arg
. Entre los dos, a.setPtr
invalida el valor f.ptr
original, que sin embargo todavía está presente en la copia a nivel de bits, y por pura coincidencia _strdup
devuelve ese mismo puntero (aunque con una nueva cadena en el interior). Otro resultado del mismo código podría ser un bloqueo en _strdup
.
Tenga en cuenta que este diseño funciona muy bien para los tipos de POD; solo se desmorona cuando los constructores y destructores necesitan efectos secundarios.
El punto original de que las convenciones de llamada y los mecanismos de paso de parámetros no necesariamente soportan la construcción no trivial y la destrucción de objetos sigue en pie: esto es exactamente lo que sucede aquí.
EDIT: respuesta originalmente dijo que el comportamiento de construcción y destrucción era específico de cdecl; No lo es. (Gracias Cody!)
Supongo que el problema es / fue la violación del tipo de seguridad. En general, pasar un objeto de clase derivado donde se espera un objeto de clase base debería ser seguro. Si el objeto de la clase base se toma por valor, entonces el objeto de la clase derivada simplemente se dividirá. Si se toma por puntero / referencia, el puntero / referencia al objeto de clase derivado se ajusta correctamente durante la compilación. Esto no funciona con las funciones de argumento variable, donde la interpretación de los tipos de entrada se realiza mediante el código en lugar de por el compilador.
Ejemplo:
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);
EDITAR
Como se señaló en un comentario ahora eliminado, A
, B
y D
en el ejemplo anterior son en realidad tipos de POD. Sin embargo, el problema que les estoy llamando tiene que ver con la herencia, que, aunque permite tipos de POD, pero en la mayoría de los casos involucra tipos que no son de POD.
Estoy grabando esto, porque es demasiado grande para ser un comentario, y fue bastante lento para cazar esto, por lo que nadie más pierde el tiempo mirando hacia abajo en esta ruta.
El texto se cambió por primera vez a algo similar a la redacción actual en el proyecto de norma en N2134 publicado el 2006-11-03.
Con un poco de esfuerzo, pude rastrear la redacción a DR506 .
El documento J16 / 04-0167 = WG21 N1727 sugiere que pasar un objeto que no sea POD a puntos suspensivos esté mal formado. Sin embargo, en las discusiones en la reunión de Lillehammer, el CWG consideró que la categoría recientemente aprobada de comportamiento con apoyo condicional sería más apropiada.
El documento al que se hace referencia ( N1727 ), dice muy poco sobre el tema:
La redacción existente (5.2.2¶7) hace que el comportamiento no definido pase un objeto no POD a una elipsis en una llamada de función:
{Recorte}
Una vez más, el CWG no vio razón para no requerir implementaciones para emitir un diagnóstico en tales casos.
Sin embargo, esto no me dice mucho acerca de por qué era la forma en que se encontraba, que es lo que quieres saber. Para mí, no es posible volver atrás el reloj al momento en que se escribió el idioma por primera vez, porque el borrador de norma más antiguo disponible gratuitamente es de 2005 y ya tiene el texto que se está preguntando, todas las normas antes de esto requieren autenticación o son simplemente sin contenido