Home > Blockchain >  A better variadic function then stdarg in C?
A better variadic function then stdarg in C?

Time:03-30

I looking to create a variadic function in C that allows to do something like this:

Send({1,2,3,4,5});
Send({4,5,2});
Send({1,1,1,1,1,1,1,1,1,1,1,1,1});

Note there is no length input and the array is placed inline and without any setup or creation of any variable

Currently i am using formal variadic option such as below (example from here), which is quite convenient but also prone to mistakes which are sometimes hard to debug such as forgetting to place the num_args (still compiles), placing the wrong number of elements etc.

int sum(int num_args, ...) {
   int val = 0;
   va_list ap;
   int i;

   va_start(ap, num_args);
   for(i = 0; i < num_args; i  ) {
      val  = va_arg(ap, int);
   }
   va_end(ap);
 
   return val;
}

CodePudding user response:

The typical way to define a function that operates on an arbitrary number of arguments of the same type is to use an array:

int sum(const int array[], size_t n)
{
    int sum = 0;
    
    while (n--) sum  = array[n];
    
    return sum;
}

That would mean that you would have to create an auxiliary array for each call and invoke the function, perhaps with a countof macro that uses sizeof to determine the size of the array and its first member.

As of C99, you can use compound literals to create such arrays:

int s = sum((int[]){1, 1, 2, 3, 5}, 5);

That might be more convenient and typesafe on the array elements, but still has the danger of getting the count wrong.

You can use a variadic macro to combine compound literals and the countof idiom:

#define SUM(...) sum((int[]){__VA_ARGS__},    \
    sizeof((int[]){__VA_ARGS__}) / sizeof(int))

(The compound literal argument of the sizeof will only be evaluated for its size.)

Use it like this:

printf("%d\n", SUM(1, 2, 3, 4, 5));
printf("%d\n", SUM(200.0, 30.0, 5.0));
printf("%d\n", SUM());

(I'm not sure whether such a macro is useful, though. The sum example is contrived at best: sum(a, b, c) can be written as a b c. Perhaps a min or max macro for more than two arguments might be useful. In general, I find that I have the data I want in array form already when I work in C.)

CodePudding user response:

If you don't want to pass length, you can use a sentinel value to mark the end. If you don't require full range of int, just use (for example) INT_MIN.

Send(1,1,1,1,1,1,1,1,1,1,1,1,1, INT_MIN);

And use that as end condition in your function's loop.

If you need full range of 32 bits, you could pass 64 bit integers so sentinel value can be outside the range of 32 bits. This will be a bit clunky though, you need to make all the parameters be 64 bit then, probably with suffix ll, and this will also make the code less portable/future proof.

CodePudding user response:

One alternative is to use strings. Depending on your purpose, this can be quite robust, because compiler will warn about invalid format string. Here's the code I tested on gcc, and I belive it also works on clang. I hope it is sufficiently self-explanatory with the comments.

The __attribute__ ((format (printf, 1, 2))) part is quite important, it is the only thing making this robust, because it allows the compiler to check the arguments and produce warnings.

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


// helper function to get a single string,
// just like vsprintf, except
// returns malloc buffer containing the result text
char *buf_print(const char *fmt, va_list ap) {
    int bufsize = 0;
    char * buf = NULL;

    {
        // get the exact size needed for the string,
        // ap can only be processed once, a copy is needed
        va_list ap2;
        va_copy(ap2, ap);
        bufsize = vsnprintf(NULL, 0, fmt, ap2);
        va_end(ap2);
    }

    if (bufsize >= 0) {
        buf = malloc(bufsize   1);
        if (buf) {
            bufsize = vsnprintf(buf, bufsize   1, fmt, ap);

            if (bufsize < 0) {
                free(buf);
                buf = NULL;
            }
        }
    }
    return buf;
}

// get next parseable integer from a string,
// skipping any leading invalid characters,
// and returning pointer for next character, as well as number in *out,
// or returning NULL if nothing could be parsed before end of string
char *parse_next_ll(char *buf, long long *out) {
    char *endp = NULL;
    while (*buf) {
        *out = strtoll(buf, &endp, 10);
        if (endp != buf) return endp; // something was parsed successfully
          buf; // nothing was parsed, try again from the next char
    };
    return NULL; // means *out does not contain valid number
}

// Go through all integers in formatted string, ignoring invalid chars,
// returning -1 on error, otherwise number of valid integers found 
int
__attribute__ ((format (printf, 1, 2)))
Send(const char *fmt, ...)
{
    int count = -1;

    va_list ap;
    va_start(ap, fmt);
    char * const buf = buf_print(fmt, ap); // needs to be freed, so value must not be lost, so const
    va_end(ap);

    if (buf) {
        count = 0;
        long long number = 0;
        char *s = buf;
        while (s && *s) {
            s = parse_next_ll(s, &number);
            if (s) {
                  count;
                printf("Number %d: %lld\n", count, number);
            }
        }
        free(buf);
    }
    return count;
}

int main()
{
    int r = Send("1,2,3,4,%i", 1);
    printf("Result: %d\n", r);
    return 0;
}

Ouptut of that code:

Number 1: 1
Number 2: 2
Number 3: 3
Number 4: 4
Number 5: 1
Result: 5
  • Related