2009-05-14 23 views
10

Estoy escribiendo una API que actualiza MUCHOS campos diferentes en una estructura.¿Es una buena idea usar varargs en una API C para establecer pares de valores clave?

que podía ayudar a la adición de campos futuros, haciendo la función de actualización variadic:

update(FIELD_NAME1, 10, FIELD_NAME2, 20); 

luego añadir FIELD_NAME3 con fuera de cambiar cualquier llamadas existentes:

update(FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30); 

palabras de sabiduría por favor?

Respuesta

11

En general, no.

Varargs arroja mucha seguridad tipográfica - se pueden pasar punteros, flotadores, etc., en lugar de ints y se compilará sin problemas. El uso indebido de variables, como la omisión de argumentos, puede ocasionar fallos inesperados debido a la corrupción de la pila o la lectura de punteros no válidos.

Por ejemplo, la siguiente llamada va a compilar y dar lugar a accidentes u otro comportamiento extraño:

UpdateField(6, "Field1", 7, "Field2", "Foo"); 

El inicial 6 es el número de parámetros que esperan. Convertirá el puntero de cadena "Foo" en int para colocarlo en Field2, y tratará de leer e interpretar otros dos parámetros que no están presentes, lo que probablemente provocará un bloqueo aquí al eliminar el ruido de la pila.

Creo que la implementación de varargs en C es un error (dado el entorno actual, probablemente tuvo perfecto sentido en 1972). La implementación consiste en pasar un montón de valores en la pila y luego el callee recorrerá la pila up parámetros, en función de su interpretación de algún parámetro de control inicial. Este tipo de implementación básicamente exige que cometas un error en lo que podría ser una forma muy difícil de diagnosticar. La implementación de esto por parte de C#, al pasar una matriz de objetos con un atributo en el método, debe ser más correcto, aunque no se puede mapear directamente en el lenguaje C.

+3

luego pase una cadena grande en un formato conocido. Las cadenas son más serializables y muy portátiles. Solo tiene que definir un formato para pasarlos - XMl es uno (aunque pesado), o simplemente lo haría una csv simple. – gbjbaanb

+1

+1 a gbjbaanb - mira (x) printf - la implementación más hermosa de los varags conocidos :) – KevinDTimm

5

Un problema con varargs en C es que no se sabe cuántos argumentos se pasan, por lo que necesita que, como otro parámetro:

update(2, FIELD_NAME1, 10, FIELD_NAME2, 20); 

update(3, FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30); 
+1

¿Por qué -1 esto? Es una respuesta totalmente válida (y correcta) a la pregunta. El OP no tiene manera de saber cuántos argumentos se van a pasar ... Y ese es un muy buen ejemplo de problemas con el uso de varargs. –

+0

Pasar un NULL como último parámetro es probablemente una mejor manera 'update (FIELD_NAME1, 10, FIELD_NAME2, 20, NULL);' ya que no necesitará editar el parámetro firs para agregar otro campo –

+0

@ MustafaSerdarŞanlı - Eso está asumiendo ' NULL' es un 'FIELD_NAME' válido. Supongo que 'FIELD_NAME's son valores' enum', por lo que necesitaría un valor especial 'END_FIELD'. Usar 'NULL' es bueno cuando cabe, pero no siempre se ajusta. –

2

Me gustaría tener una larga y dura mirada en cualquier "actualización "funcionalidad diseñada para ser utilizada externamente (o incluso internamente) que usa la misma función para actualizar muchos campos diferentes en una estructura. ¿Hay alguna razón específica por la que no puede tener una funcionalidad discreta para actualizar los campos?

3

¿Por qué no tener una arg, una matriz. Mejor aún, un puntero a una matriz.

struct field { 
    int val; 
    char* name; 
}; 

o incluso ...

union datatype { 
    int a; 
    char b; 
    double c; 
    float f; 
// etc; 
}; 

continuación

struct field { 
    datatype val; 
    char* name; 
}; 

union (struct* field_name_val_pairs, int len); 

ok 2 args. Mentí, y pensé que un parámetro de longitud sería bueno.

+0

Creo que este es un buen diseño, excepto que evitaría la unión (simplemente dale un tipo fijo a val). De esta forma, el compilador puede ayudarlo a aplicar la seguridad del tipo. Por supuesto, la duración aún tiene que ser correcta. –

+0

Esto es bueno, y update_ary (int n, char ** names, datatype * vals) es posible aunque un poco incómodo a veces. – dmckee

+0

Usar matrices en lugar de varargs puede ser una buena decisión, pero también puede volverse realmente desagradable cuando el tipo de valores es variable. Solo piense en su código lleno de variables 'void **' ... – brandizzi

6

Tiendo a evitar varargs excepto en una circunstancia específica donde es muy útil. Los argumentos variables en realidad no ofrecen mucho beneficio por encima de lo que se puede hacer mediante llamadas a funciones individuales, especialmente en su caso.

En términos de legibilidad (y eso es generalmente lo que prefiero sobre la velocidad bruta, excepto en casos muy específicos), no hay diferencia real entre las dos opciones siguientes (He agregado un conteo a las versiones varargs ya que necesita una contar o centinela para detectar el final de los datos):

update(2, FIELD_NAME1, 10, FIELD_NAME2, 20); 
update(3, FIELD_NAME3, 10, FIELD_NAME4, 20, FIELD_NAME5, 30); 
/* ========== */ 
update(FIELD_NAME1, 10); 
update(FIELD_NAME2, 20); 
update(FIELD_NAME3, 10); 
update(FIELD_NAME4, 20); 
update(FIELD_NAME5, 30); 

de hecho, como la versión varargs se hace más largo, tendrá que dividirlo todos modos, para el formato:

update(5, 
    FIELD_NAME1, 10, 
    FIELD_NAME2, 20, 
    FIELD_NAME3, 10, 
    FIELD_NAME4, 20, 
    FIELD_NAME5, 30); 

Hacer es el resultado de "una llamada por nombre de campo" en un código más simple i n la función en sí misma, y ​​no degrada la legibilidad de las llamadas. Además, permite que el compilador detecte ciertos errores que no puede hacer para varargs, como tipos incorrectos o una discrepancia entre el recuento proporcionado por el usuario y el recuento real.

Si realmente debe ser capaz de llamar a una función única de hacerlo, me gustaría optar por:

void update (char *k1, int v1) { 
    ... 
} 
void update2 (char *k1, int v1, char *k2, int v2) { 
    update (k1, v1); 
    update (k2, v2); 
} 
void update3 (char *k1, int v1, char *k2, int v2, char *k3, int v3) { 
    update (k1, v1); /* or these two could be a single */ 
    update (k2, v2); /* update2 (k1, v1, k2, v2); */ 
    update (k3, v3); 
} 
/* and so on. */ 

Incluso se puede hacer las funciones de nivel superior como macros, si lo prefiere, sin perder seguridad tipo.

El único lugar donde suelo usar funciones varargs es cuando proporciono la misma funcionalidad que printf() - por ejemplo, ocasionalmente he tenido que escribir logging libraries con funciones como logPrintf() que proporcionan la misma funcionalidad. No puedo pensar en cualquier otra vez en mi larga (y quiero decir, larga :-) vez en la coalface que he necesitado para usarla.

Como un lado, si decides usar varargs, tiendo a usar centinelas en lugar de conteos ya que esto evita discrepancias al agregar campos. Desde aquí se puede olvidar para ajustar el recuento y terminar con:

update (2, k1, v1, k2, v2, k3, v3); 

al agregar, que es insidioso, ya que en silencio se salta K3/v3, o:

update (3, k1, v1, k2, v2); 

cuando se eliminan, lo que es casi seguro fatal para el funcionamiento exitoso de su programa.

Tener un centinela evita que esto (siempre y cuando que no se olvide el centinela, por supuesto):

update (k1, v1, k2, v2, k3, v3, NULL); 
1

Mucha gente aquí han sugerido que pasa el # de parámetros, sin embargo otros señalan que con razón esto lleva a errores insidiosos donde el número de campos cambia pero el recuento pasado a la función vararg no lo hace. Resuelvo esto en un producto mediante el uso de terminación nula en su lugar:

send_info(INFO_NUMBER, 
      Some_Field,  23, 
      Some_other_Field, "more data", 
      NULL); 

esta manera, cuando los programadores de copiar y pegar, inevitablemente, lo copian, no son propensos a estropear. Y más importante aún, no es probable que lo arruine.

Al volver al problema original, tiene una función que debe actualizar una estructura con muchos campos, y la estructura crecerá.El método habitual (en las API clásicas de Win32 y MacOS) de pasar datos de este tipo a una función es pasar otra estructura (incluso puede ser la misma que la que está actualizando), es decir:

void actualización (UPDATESTRUCTURE * update_info);

usarlo, usted rellenar los campos:

UPDATESTRUCTURE my_update = { 
    UPDATESTRUCTURE_V1, 
    field_1, 
    field_2 
}; 
update(&my_update); 

Más tarde, cuando se añaden nuevos campos, puede actualizar la definición UPDATESTRUCTURE y recompilar. Al ingresar la versión #, puede admitir un código anterior que no usa los nuevos campos.

Una variante del tema es tener un valor para los campos que no desea actualizar, como KEEP_OLD_VALUE (idealmente será 0) o NULL.

UPDATESTRUCTURE my_update = { 
    field_1, 
    NULL, 
    field_3 
}; 
update(&my_update); 

no incluyo una versión porque tomo ventaja del hecho cuando aumentamos el # de campos en UPDATESTRUCTURE, los campos adicionales se inicializará a 0, o KEEP_OLD_VALUE.

+0

Tal diseño no es muy seguro, y sus ejemplos contienen posibles errores. Como NULL puede definirse como solo "0", que es de tipo int, y el tamaño de int puede ser diferente al tamaño de los punteros, puede haber una discrepancia. Por lo tanto * SIEMPRE * se envía a un puntero para argumentos NULL. Ver http://www.lysator.liu.se/c/c-faq/c-1.html. – hlovdal

+0

Ese es un buen punto: en el ejemplo vararg, no especifico los tipos de Some_Field y Some_other_Field. Si no se implementan como enteros, sería aconsejable convertir el NULL al mismo tipo. – Paul

2

Las razones dadas hasta ahora para evitar varargs son todas buenas. Permítanme agregar otro que aún no se haya proporcionado, ya que es menos importante, pero se puede encontrar. El vararg ordena que el parámetro se pase en la pila, lo que ralentiza la llamada a la función. En alguna arquitectura, la diferencia puede ser significativa. En x86 no es muy importante debido a su falta de registro, en SPARC, por ejemplo, puede ser importante. Se pasan hasta 5 parámetros en los registros y si su función usa pocos locales, no se realiza ningún ajuste de pila. Si su función es una función de hoja (es decir, no llama a otra función), tampoco hay ajuste de ventana. El costo de la llamada es muy pequeño. Con un vararg, la secuencia normal de pasar parámetros en la pila, el ajuste de la pila y la gestión de ventanas se realiza o su función no podría obtener los parámetros. Esto aumenta el costo de la llamada de manera significativa.

Cuestiones relacionadas