Escribiendo una función genérica en C, cómo manejar cadenas

Tengo una función que toma un argumento void** y un entero que indica su tipo de datos

 void foo (void** values, int datatype) 

Dentro de la función, dependiendo del tipo de datos, lo hago de esta manera:

 if (datatype == 1) *values = (int*) malloc (5 * sizeof(int)); else if (datatype == 2) *values = (float*) malloc (5 * sizeof(float)); 

Todo está bien hasta ahora. Sin embargo, cuando las cadenas de caracteres entran en escena, las cosas se complican. El void** debería ser void*** , ya que tendré que hacer algo como esto:

 *values = (char**) malloc (5 * sizeof(char*)); for(i=0;i<5;i++) (*values)[i] = (char*) malloc (10); .. strncpy( (*values)[0], "hello", 5); 

¿Cómo debería manejarse tal situación? ¿Puedo pasar un char*** a la función que espera un void** pero lanzarlo correctamente dentro de él?

 void foo (void** values, int datatype) { if(datatype == 3) { char*** tmp_vals = (char***) values; *tmp_vals = (char**) malloc (5 * sizeof(char*)); ... (*tmp_vals)[i] = (char*) malloc (10 * sizeof(char)); strncpy ( (*tmp_vals)[i], "hello", 5); } 

Así que acabo de lanzar el void** en un char*** . Intenté esto e ignorando las advertencias, funcionó bien. ¿Pero es esto seguro? ¿Hay una alternativa más elegante?

No necesita (y probablemente no deba) usar un void ** en absoluto, solo use un void * regular void * . Según C11 6.3.2.3.1, “un puntero a void se puede convertir en un puntero a cualquier tipo de objeto. Un puntero a cualquier tipo de objeto se puede convertir en un puntero a void y viceversa; el resultado se comparará igual a el puntero original “. Una variable de puntero, que incluye un puntero a otro puntero, es un objeto. void ** no es “un puntero a void “. Puede realizar conversiones de forma gratuita y segura desde y hacia el void * , pero no se garantiza que pueda realizar la conversión de forma segura desde y hacia el void ** .

Así que solo puedes hacer:

 void foo (void* values, int datatype) { if ( datatype == 1 ) { int ** pnvalues = values; *pnvalues = malloc(5 * sizeof int); /* Rest of function */ } 

y así sucesivamente, y luego llamarlo similar a:

 int * new_int_array; foo(&new_int_array, 1); 

&new_int_array es del tipo int ** , que se convertirá implícitamente a void * por foo() , y foo() lo convertirá de nuevo al tipo int ** y new_int_array modificación indirecta de new_int_array para apuntar a la nueva memoria que tiene dinámicamente asignado.

Para un puntero a una matriz dinámica de cadenas:

 void foo (void* values, int datatype) { /* Deal with previous datatypes */ } else if ( datatype == 3 ) { char *** psvalues = values; *psvalues = malloc(5 * sizeof char *); *psvalues[0] = malloc(5); /* Rest of function */ } 

Y así sucesivamente, y llámalo:

 char ** new_string_array; foo(&new_string_array, 3); 

De manera similar, &new_string_array es de tipo char *** , nuevamente se convierte implícitamente a void * , y foo() convierte de nuevo e indirectamente hace que new_string_array señale los bloques de memoria recién asignados.

¿Cómo debería manejarse tal situación? ¿Puedo pasar un char*** a la función que espera un void** pero lanzarlo correctamente dentro de él?

No, eso es un comportamiento técnicamente indefinido. Puede parecer que funciona en su computadora, pero puede fallar en alguna computadora futura que implemente diferentes tipos de punteros con diferentes representaciones, lo que está permitido por el estándar de lenguaje C.

Si su función espera un void** , entonces es mejor que pase un void** . Cualquier tipo de puntero se puede convertir implícitamente a void* , pero eso solo funciona en el nivel superior: char* se puede convertir a void* , y char** se puede convertir implícitamente a void* (porque char** es “puntero a char* “), pero char** no se puede convertir a void** , y del mismo modo char*** tampoco se puede convertir a void** .

La forma correcta de llamar a esta función es pasarle un void** adecuado void** , luego devolver el puntero void* resultante a su tipo original:

 void foo(void **values, int datatype) { if(datatype == 3) { char ***str_values = ...; *values = str_values; // Implicit cast from char*** to void* } else ... } ... void *values; foo(&values, 2); char ***real_values = (char ***)values; 

Suponiendo que los *values se apuntaron realmente a un char*** , entonces esta conversión es válida y no tiene ningún comportamiento indefinido en ninguna de las rutas de código.

Un void * es solo un puntero a un tipo no especificado; puede ser un puntero a un int , o un char , o un char * , o un char ** , o cualquier cosa que desee, siempre y cuando se asegure de que, cuando no lo haga, lo trate como el tipo apropiado (o uno que el tipo original podría ser interpretado con seguridad como).

Por lo tanto, un void ** es solo un puntero a un void * , que podría ser un puntero a cualquier tipo que desee, como un char * . Entonces sí, si está asignando matrices de algunos tipos de objetos, y en un caso esos objetos son char * , entonces podría usar un void ** para referirse a ellos, dándole algo que podría llamarse un char *** .

Por lo general, es poco común ver esta construcción directamente, porque generalmente se adjunta algún tipo o información de longitud a la matriz, en lugar de tener un char *** tiene una struct typed_object **foo o algo por el estilo donde struct typed_object tiene una etiqueta de tipo y el puntero, y convierte el puntero que extrae de esos elementos a los tipos adecuados, o tiene una struct typed_array *foo que es una estructura que contiene un tipo y una matriz.

Un par de notas sobre el estilo. Por un lado, hacer este tipo de cosas puede hacer que su código sea difícil de leer. Tenga mucho cuidado de estructurarlo y documentarlo con claridad para que las personas (incluido usted) puedan descubrir qué está pasando. Además, no lances el resultado de malloc ; el void * promueve automáticamente el tipo al que está asignado, y la conversión del resultado de malloc puede llevar a errores sutiles si olvida incluir o actualizar la statement de tipo pero olvida actualizar la conversión. Vea esta pregunta para más información.

Y generalmente es un buen hábito adjuntar el * en una statement al nombre de la variable, no el nombre del tipo, ya que así es como se analiza. Lo siguiente declara un caracter y un char * , pero si lo escribe de la forma en que los ha escrito, puede esperar que declare dos caracteres char * :

 char *foo, bar; 

O escrito de otra manera:

 char* foo, bar; 

Hay un mecanismo incorporado para hacer esto ya con la ventaja añadida de que permite un número variable de argumentos. Se ve comúnmente en este formato yourfunc(char * format_string,...)

 /*_Just for reference_ the functions required for variable arguments can be defined as: #define va_list char* #define va_arg(ap,type) (*(type *)(((ap)+=(((sizeof(type))+(sizeof(int)-1)) \ & (~(sizeof(int)-1))))-(((sizeof(type))+ \ (sizeof(int)-1)) & (~(sizeof(int)-1))))) #define va_end(ap) (void) 0 #define va_start(ap,arg) (void)((ap)=(((char *)&(arg))+(((sizeof(arg))+ \ (sizeof(int)-1)) & (~(sizeof(int)-1))))) */ 

Este es un ejemplo básico que podría usar con una cadena de formato y un número variable de argumentos

 #define INT '0' #define DOUBLE '1' #define STRING '2' void yourfunc(char *fmt_string, ...){ va_list args; va_start (args, fmt_string); while(*fmt_string){ switch(*fmt_string++){ case INT: some_intfxn(va_arg(ap, int)); case DOUBLE: some_doublefxn(va_arg(ap, double)); case STRING: some_stringfxn(va_arg(ap, char *)); /* extend this as you like using pointers and casting to your type */ default: handlfailfunc(); } } va_end (args); } 

Así que puedes ejecutarlo como: yourfunc("0122",42,3.14159,"hello","world"); o porque solo quería que 1 comenzara con su yourfunc("1",2.17); No hay mucho más genérico que eso. Incluso podría configurar varios tipos de enteros para decirle que ejecute un conjunto diferente de funciones en ese entero particular. Si format_string es demasiado tedioso, entonces puede usar el int datatype de int datatype en su lugar, pero estaría limitado a 1 arg (técnicamente, podría usar operaciones de bits para OR tipo de datos | num_args, pero estoy divagando)

Aquí está la forma de un valor de un tipo:

 #define INT '0' #define DOUBLE '1' #define STRING '2' void yourfunc(datatype, ...){ /*leaving "..." for future while on datatype(s)*/ va_list args; va_start (args, datatype); switch(datatype){ case INT: some_intfxn(va_arg(ap, int)); case DOUBLE: some_doublefxn(va_arg(ap, double)); case STRING: some_stringfxn(va_arg(ap, char *)); /* extend this as you like using pointers and casting to your type */ default: handlfailfunc(); } va_end (args); } 

Con algunos trucos, puedes hacerlo. Ver ejemplo:

 int sizes[] = { 0, sizeof(int), sizeof(float), sizeof(char *) } void *foo(datatype) { void *rc = (void*)malloc(5 * sizes[datatype]); switch(datatype) { case 1: { int *p_int = (int*)rc; for(int i = 0; i < 5; i++) p_int[i] = 1; } break; case 3: { char **p_ch = (char**)rc; for(int i = 0; i < 5; i++) p_ch[i] = strdup("hello"); } break; } // switch return rc; } // foo 

En el llamador, simplemente convierta el valor devuelto al puntero apropiado y trabaje con él.