Funciones que devuelven cuerdas, ¿buen estilo?

En mis programas C, a menudo necesito una forma de hacer una representación de cadena de mis ADT. Incluso si no necesito imprimir la cadena en la pantalla de ninguna manera, es bueno tener ese método para depurar. Así que este tipo de función aparece a menudo.

char * mytype_to_string( const mytype_t *t ); 

De hecho, me doy cuenta de que tengo (al menos) tres opciones aquí para manejar la memoria para que la cadena regrese.

Alternativa 1: Almacenar la cadena de retorno en una matriz de caracteres estáticos en la función No necesito pensar mucho, excepto que la cadena se sobrescribe en cada llamada. Lo cual puede ser un problema en algunas ocasiones.

Alternativa 2: Asignar la cadena en el montón con malloc dentro de la función. Realmente ordenado, ya que entonces no necesitaré pensar en el tamaño de un búfer o la sobrescritura. Sin embargo, debo recordar liberar () la cadena cuando termine, y luego también debo asignar una variable temporal tal que Puedo liberar. Y luego la asignación de pila es mucho más lenta que la asignación de pila, por lo tanto, será un cuello de botella si esto se repite en un bucle.

Alternativa 3: pasar el puntero a un búfer y dejar que el llamador asigne ese búfer. Como:

char * mytype_to_string( const mytype_t *mt, char *buf, size_t buflen ); 

Esto trae más esfuerzo a la persona que llama. También noto que esta alternativa me da otra opción en el orden de los argumentos. ¿Qué argumento debería tener primero y último? (En realidad seis posibilidades)

Entonces, ¿cuál debería preferir? ¿Por qué? ¿Existe algún tipo de estándar no escrito entre los desarrolladores de C?

Comentarios

  • Solo una nota de observación, la mayoría de los sistemas operativos usan la opción 3: el llamador asigna el búfer de todos modos; le dice al puntero y la capacidad del búfer; el destinatario de la llamada llena el búfer er y también devuelve la longitud real de la cadena si el búfer es insuficiente. Ejemplo: sysctlbyname en OS X e iOS

Respuesta

Los métodos que más he visto son 2 y 3.

El búfer proporcionado por el usuario es bastante sencillo de usar:

char[128] buffer; mytype_to_string(mt, buffer, 128); 

Aunque la mayoría de las implementaciones devolverán la cantidad de búfer usada.

La opción 2 será más lenta y peligrosa cuando se usan bibliotecas vinculadas dinámicamente donde pueden usar diferentes tiempos de ejecución (y diferentes montones). Por lo tanto, no puede liberar lo que se ha mal ubicado en otra biblioteca. Esto requiere una función free_string(char*) para solucionarlo.

Comentarios

  • ¡Gracias! Creo que también me gusta más la Alternativa 3. Sin embargo, quiero poder hacer cosas como: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); y, por lo tanto, gané ‘ t me gustaría devolver la longitud utilizada, sino el puntero a la cuerda. El comentario dinámico de la biblioteca es realmente importante.
  • No debería ‘ ser sizeof(buffer) - 1 para satisfacer la \0 terminator?
  • @ Michael-O no, el término nulo se incluye en el tamaño del búfer, lo que significa que la cadena máxima que se puede introducir es 1 menos que el tamaño pasado. Este es el patrón que utiliza la cadena segura en la biblioteca estándar, como snprintf.
  • @ratchetfreak Gracias por la aclaración. Sería bueno extender la respuesta con esa sabiduría.

Responder

Idea de diseño adicional para # 3

Cuando sea posible, proporcione también el tamaño máximo necesario para mytype en el mismo archivo .h que mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Ahora el usuario puede codificar en consecuencia.

char buf[MYTYPE_TO_STRING_SIZE]; puts(mytype_to_string(mt, buf, sizeof buf)); 

Pedido

El tamaño de las matrices, cuando es el primero, permite tipos de VLA.

char * mytype_to_string( const mytype_t *mt, size_t bufsize, char *buf[bufsize]); 

No tan importante con una sola dimensión, pero útil con 2 o más.

void matrix(size_t row, size_t col, double matrix[row][col]); 

Recuerdo leer que tener el tamaño primero es un modismo preferido en la siguiente C. Necesito encontrar esa referencia ….

Respuesta

Como adición a la excelente respuesta de @ratchetfreak, señalaría que la alternativa n. ° 3 sigue un paradigma / patrón similar a las funciones de la biblioteca C estándar.

Por ejemplo, strncpy.

 char * strncpy ( char * destination, const char * source, size_t num );  

Seguir el mismo paradigma ayudaría para reducir la carga cognitiva de los nuevos desarrolladores (o incluso de su futuro yo) cuando necesiten usar su función.

La única diferencia con lo que tiene en su publicación sería que el en las bibliotecas de C tiende a aparecer primero en la lista de argumentos.Entonces:

 char * mytype_to_string( char *buf, const mytype_t *mt, size_t buflen );  

Respuesta

Me echo a @ratchet_freak en la mayoría de los casos (tal vez con un pequeño ajuste para sizeof buffer sobre 128) pero quiero saltar aquí con una respuesta extraña. ¿Qué tal ser raro? ¿Por qué no, además de los problemas de obtener miradas extrañas de nuestros colegas y tener que ser más persuasivo? Y ofrezco esto:

// Note allocator parameter. char* mytype_to_string(allocator* alloc, const mytype_t* t) { char* buf = allocate(alloc, however_much_you_need); // fill out buf based on "t" contents return buf; } 

Y uso de ejemplo:

void func(my_type a, my_type b) { allocator alloc = allocator_new(); const char* str1 = mytype_to_string(&alloc, &a); if (!str1) goto oom; const char* str2 = mytype_to_string(&alloc, &b); if (!str2) goto oom // do something with str1 and str2 goto finish; oom: errno = ENOMEM; finish: // Frees all memory allocated through `alloc`. allocator_purge(&alloc); } 

Si lo hizo de esta manera, puede hacer que su asignador sea muy eficiente (más eficiente que malloc tanto en términos de costos de asignación / desasignación como de una localidad de referencia mejorada para el acceso a la memoria). Puede ser un asignador de arena que solo implica aumentar un puntero en casos comunes para solicitudes de asignación y agrupar la memoria de forma secuencial a partir de grandes bloques contiguos (el primer bloque ni siquiera requiere un montón ubicación: se puede asignar en la pila). Simplifica el manejo de errores. Además, y este podría ser el más debatible, pero creo que es prácticamente bastante obvio en términos de cómo deja en claro a la persona que llama que va a asignar memoria al asignador que usted pasa, requiriendo una liberación explícita (allocator_purge en este caso) sin tener que documentar dicho comportamiento en cada una de las funciones posibles si usa este estilo de manera consistente. La aceptación del parámetro asignador lo hace, con suerte, bastante obvio.

No lo sé. Aquí obtengo argumentos en contra, como implementar el asignador de arena más simple posible (solo use la alineación máxima para todas las solicitudes) y lidiar con esto es demasiado trabajo. Mi pensamiento contundente es como, ¿qué somos, programadores de Python? También podríamos usar Python si es así. Por favor, use Python si estos detalles no importan. Lo digo en serio. He tenido muchos colegas de programación en C que muy probablemente escribirían no solo un código más correcto, sino posiblemente incluso más eficiente con Python, ya que ignoran cosas como la localidad de referencia mientras tropiezan con los errores que crean a izquierda y derecha. Yo no » Veamos qué es lo que da tanto miedo a un asignador de arena simple aquí si somos programadores de C preocupados por cosas como la ubicación de los datos y la selección de instrucciones óptimas, y podría decirse que esto es mucho menos para pensar que al menos los tipos de interfaces que requieren llamadores para liberar explícitamente cada cosa individual que la interfaz puede devolver. Ofrece desasignación masiva sobre desasignación individual de un tipo que es más propenso a errores. Un programador de C adecuado desafía las llamadas loopy a malloc como yo lo veo, especialmente cuando aparece como un punto de acceso en su generador de perfiles. Desde mi punto de vista, tiene que haber más » oomph » como justificación para seguir siendo un programador de C en 2020, y podemos » Evite más cosas como los asignadores de memoria.

Y esto no tiene los casos extremos de asignar un búfer de tamaño fijo donde asignamos, digamos, 256 bytes y la cadena resultante es más grande. Incluso si nuestras funciones evitan el desbordamiento del búfer (como con sprintf_s), hay más código para recuperarse adecuadamente de tales errores que es necesario y que podemos omitir con el caso del asignador, ya que no «No tenemos esos casos extremos. No tenemos que lidiar con tales casos en el caso del asignador, a menos que realmente agotemos el espacio de direccionamiento físico de nuestro hardware (que maneja el código anterior, pero no tiene que lidiar con » sin búfer preasignado » por separado de » sin memoria «).

Respuesta

Además del hecho de que lo que está proponiendo hacer es mal olor a código, la alternativa 3 me suena mejor. También creo que, como @ gnasher729, estás usando el idioma incorrecto.

Comentarios

Respuesta

Para ser honesto, es posible que desee cambiar a un idioma diferente donde devolver una cadena no sea una operación compleja, que requiere mucho trabajo y propensa a errores.

Podría considerar C ++ o Objective-C, donde podría dejar el 99% de su código sin cambios.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *