1

I've found that there are conditions that the second argument of va_start(ap, last) must satisfy, which are:

  • last must not be a register variable
  • last must not be a function
  • last must not be an array

(Sources: stdarg(3), cppreference)

Though the manual page explains the reason these constraints exist, but I don't seem to understand the implication.

Does that mean the following code causes an undefined behavior? In this code, iterate_string takes a function of type processor_t and variable arguments follow this parameter.

/*
 * A program that skips or reads some characters
 * from the external variable `src` and stores
 * them into `buf`.
 */
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

/* External variables */
const char *src = "UwU Chino chan is so cute!!";
const int src_len = 22;
int src_idx = 0;  /* position in src */

char ch, buf[128];
int buf_idx;      /* position in buf */

/* Typedefs */
typedef int processor_t(va_list *ap_ptr);
typedef int checker_t(va_list *ap_ptr);

/*
 * Iterate 'src' by invoking `process`.
 * If `process` returns 0, stops the iteration.
 */
void iterate_string(processor_t process, ...) {
   va_list ap_is;  /* is = iterate string */

   while (src_idx < src_len) {
      va_start(ap_is, process);
      if (!process(&ap_is))
         break;
   }
   va_end(ap_is);
   return;
}

/*
 * Store a character into `buf`.
 * If `check` returns 0, stops the storing.
 */
int process_storing(va_list *ap_ptr) {
   checker_t *check;

   check = va_arg(*ap_ptr, checker_t *);

   ch = src[src_idx++];
   if ((*check)(ap_ptr)) {
      buf[buf_idx++] = ch;
      return 1;
   }
   else {
      buf[buf_idx] = '\0';
      return 0;
   }
}

/*
 * Just skip a character.
 * If `check` returns 0, stops the skipping.
 */
int process_leaving(va_list *ap_ptr) {
   checker_t *check;

   check = va_arg(*ap_ptr, checker_t *);

   ch = src[src_idx];
   if ((*check)(ap_ptr)) {
      src_idx++;
      return 1;
   }
   else
      return 0;
}

/*
 * Sees whether `cnt` is less than a number.
 * If not, returns.
 */
int check_count_less_than(va_list *ap_ptr) {
   static int cnt = 0;
   int n;

   n = va_arg(*ap_ptr, int);
   if (cnt < n) {
      cnt++;
      return 1;
   }
   else {
      cnt = 0;
      return 0;
   }
}

/*
 * Read `src` and store `n` characters into `buf`.
 */
void store_nchar(int n) {
   buf_idx = 0;
   iterate_string(process_storing, check_count_less_than, n);
}

/*
 * Read `src` and skip `n` characters.
 */
void leave_nchar(int n) {
   iterate_string(process_leaving, check_count_less_than, n);
}

int main(void) {
   leave_nchar(4); /* Skip "UwU " */
   store_nchar(5); /* Save "Chino" */

   // Daily greetings
   printf("Me: Hi, %s chan! How are you today?\n", buf);
   printf("%s: I'm sorry... Do I know you?\n", buf);

   return 0;
}

Although this code seems working (at least in my machine) but I'm not sure whether my code is good or bad. Should I have iterate_string have a pointer to processor_t? Please explain as easy as possible.

1
  • 1
    Hard to give a guaranteed explication to a decision of the C standard committee without being a member... IMHO the rationale is to cope with existing implementations. The most common one is to use a pointer inside the stack. The problem is that arrays, register variable or functions could have an alternate way of being passed, and that could cause compatibility problems to some implementations. As the most common use cases (the printf and scanf families) only need a pointer, the restriction was probably deemed acceptable. Commented 2 hours ago

1 Answer 1

3

This appears to be a defect in the C11 and earlier standards.


The manpage passage you quoted is derived from section 7.16.1.4p4 of the C11 standard regarding va_start:

The parameter parmN is the identifier of the rightmost parameter in the variable parameter list in the function definition (the one just before the , ...). If the parameter parmN is declared with the register storage class, with a function or array type, or with a type that is not compatible with the type that results after application of the default argument promotions, the behavior is undefined.

Since va_start is a macro and not a function, parameters that have array or function type don't undergo pointer adjustment as would happen for parameters passed to a function. So the constraints against function and array types was likely added for this reason.

The problem with this is that what would get passed in as the second argument to va_start is a parameter of the calling function, and function parameters can't actually have array or function type.

Section 6.7.6.3 regarding function declarations says the following in paragraph 7 regarding array parameters:

A declaration of a parameter as ‘‘array of type’’ shall be adjusted to ‘‘qualified pointer to type’’

And paragraph 8 states the following regarding function parameters:

A declaration of a parameter as ‘‘function returning type’’ shall be adjusted to ‘‘pointer to function returning type’’

The definition of va_start was modified in the C23 standard to make the second parameter optional for backward compatibility, and specifically states that any parameters after the first are not evaluated.


So to summarize, your usage should be fine because the first parameter to iterate_string actually has pointer type, not function type.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.