Search code examples
cvariadic-functions

How to use a variadic argument for its own data structure?


To facilitate the use of a data structure that contains a string to be filled from a function, I would like to be able to define the same function with variadic arguments, like this:

struct my_struct_t
{
    char *msg;
};

struct my_struct_t *fill(const char *fmt, ...);

struct my_struct_t *filled = fill("A number: %d, a string: '%s'.", 43, "hello");

To do so, I implemented the following function, as well as variants, but the result was always wrong when I retrieved the string in the structure. Here is the code:

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

struct my_struct_t
{
    char *msg;
};

struct my_struct_t *fill(const char *fmt, ...)
{
    va_list ap1, ap2;
    va_copy(ap2, ap1);
    va_start(ap1, fmt);

    int slen = snprintf(NULL, 0, fmt, ap1);
    va_end(ap1);

    char *str = malloc(slen);
    assert(str != NULL);

    va_start(ap2, fmt);
    snprintf(str, slen, fmt, ap2);
    va_end(ap2);

    struct my_struct_t *my_struct = malloc(sizeof *my_struct);
    assert(my_struct != NULL);

    my_struct->msg = str;

    return my_struct;
}

int main()
{
    struct my_struct_t *filled = fill("A number: %d, a string: '%s'.", 43, "hello");
    printf("%s\n", my_struct->msg);
    return 0;
}

This leads to different results with each execution, for example:

A number: 7011816, a string: 'ðe'

I guess this is a problem with the use of variadic arguments, however I haven't found how to solve my problem, i.e. save the string in the structure field with the formatting sent, so I'll expect this:

A number: 43, a string: 'hello'.


Solution

  • A quick possible fix. Notes in code

    struct my_struct_t *fill(const char *fmt, ...) {
        va_list ap1, ap2;
    
        // change order 
        va_start(ap1, fmt);
        va_copy(ap2, ap1);
    
        // Use vsnprintf
        //int slen = snprintf(NULL, 0, fmt, ap1);
        int slen = vsnprintf(NULL, 0, fmt, ap1);
        va_end(ap1);
    
        // test result
        assert(slen >= 0);
        // ... or a pedantic test
        assert(slen >= 0 && (unsigned) slen < SIZE_MAX);
    
        // Need + 1 for null character
        // char *str = malloc(slen); 
        char *str = malloc(slen + 1u);
        assert(str != NULL);
    
        // No va_start, copy is enough
        // va_start(ap2, fmt);
    
        // snprintf(str, slen, fmt, ap2);
        // Since we a going for broke, no need for `n`, pedantically we could/should use vsnprintf()
        // vsnprintf(str, slen+1u, fmt, ap2);
        vsprintf(str, fmt, ap2);
        va_end(ap2);
    
        // Good use of sizing by referenced type
        struct my_struct_t *my_struct = malloc(sizeof *my_struct);
        assert(my_struct != NULL);
    
        my_struct->msg = str;
    
        return my_struct;
    }
    

    Rather than assert(), code could return NULL. Be sure to free resources.

    struct my_struct_t *fill(const char *fmt, ...) {
        va_list ap1, ap2;
        va_start(ap1, fmt);
        va_copy(ap2, ap1);
    
        int slen = vsnprintf(NULL, 0, fmt, ap1);
        va_end(ap1);
        if (slen < 0 || (unsigned) slen >= SIZE_MAX) {
          va_end(ap2);
          return NULL;
        }
    
        char *str = malloc(slen + 1u);
        if (str == NULL) {
          va_end(ap2);
          return NULL;
        }
    
        slen = vsnprintf(str, slen+1u, fmt, ap2);
        va_end(ap2);
        if (slen < 0 || (unsigned) slen >= SIZE_MAX) {
          free(str); 
          return NULL;
        }
    
        struct my_struct_t *my_struct = malloc(sizeof *my_struct);
        if (my_struct == NULL) {
          free(str); 
          return NULL;
        }
        my_struct->msg = str;
        return my_struct;
    }