Counting Variadic Function Arguments

I'm trying to count function arguments....

This code works okay:

#include <stdarg.h>

class Foo {
  public:
template<typename ... Args>
    Foo(Args const & ... args) {
      Serial.println(sizeof...(Args));
    }
};

void setup() {
  Serial.begin(115200);
  static Foo fooOne(1, 2, 3);
}

void loop() { }

/* OUTPUT IS: 3 */

But.... I'm trying to use it in this context so I do not need the leading argument that dictates the count.

#include <stdarg.h>

class Foo {
  public:
    byte size;
    byte* arr;

    Foo(byte size, ...) : size(size), arr(new byte [size]) {
      va_list arguments;
      va_start(arguments, size);
      for (byte i = 0; i < size; i++)
        arr[i] = va_arg(arguments, byte*);
      va_end(arguments);
    }

    void print() {
      for (byte i = 0; i < size; i++)
        Serial.print((String)arr[i] + ", ");
      Serial.println();
    }
};

void setup() {
  Serial.begin(115200);
  static Foo fooOne(3,  1, 2, 3);
  fooOne.print();
}

void loop() {
  // put your main code here, to run repeatedly:
}
/* OUTPUT PRINTS THE ARRAY: 1, 2, 3, */

I'm basically trying to use example two, but infer the number of arguments like in example one.

If I try this:

#include <stdarg.h>

class Foo {
  public:
    byte size;
    byte* arr;

    template<typename ... Args>
    Foo(Args const & ... args) : size(sizeof...(Args)), arr(new byte [sizeof...(Args)]) {
      va_list arguments;
      va_start(arguments, size);
      for (byte i = 0; i < size; i++)
        arr[i] = va_arg(arguments, byte*);
      va_end(arguments);
    }

    void print() {
      for (byte i = 0; i < size; i++)
        Serial.print((String)arr[i] + ", ");
      Serial.println();
    }
};

void setup() {
  Serial.begin(115200);
  static Foo fooOne(1, 2, 3);
  fooOne.print();
}

void loop() {
  // put your main code here, to run repeatedly:
}

I get:

error: 'va_start' used in function with fixed args
       va_start(arguments, size);

I feel the pieces are there, but I can't put it together (even after reading plenty of Google searches). I do think first the two examples above are using different things, yes? Parameter pack vs Variadic arguments or something like that?

Any helpers?

What's wrong with the first code? It seems to do what you want.

I'm trying to use it in conjunction with va_list. So like example #2, but I'm trying to irradicate that leading argument (i.e the first '3' in static Foo fooOne(3, 1, 2, 3);)

I attempted to combine them in example #3 but I haven't managed to work it out yet. Am I missing something obvious? (I hope so!).

When I attempt this it claims that size is '0' still.

#include <stdarg.h>

class Foo {
  public:
    byte size;
    byte* arr;

template<typename ... Args>
    Foo(...) : size(sizeof...(Args)), arr(new byte [sizeof...(Args)]) {
      Serial.println(size);
      va_list arguments;
      va_start(arguments, size);
      for (byte i = 0; i < size; i++)
        arr[i] = va_arg(arguments, byte*);
      va_end(arguments);
    }

    void print() {
      for (byte i = 0; i < size; i++)
        Serial.print((String)arr[i] + ", ");
      Serial.println();
    }
};

void setup() {
  Serial.begin(115200);
  static Foo fooOne(1, 2, 3);
  fooOne.print();
}

void loop() {
  // put your main code here, to run repeatedly:
}

Hello

It's not possible

I did think that until I managed to output the correct number with example #1.

Is there a way to use the template<typename ... Args> from example #1 to fill an array, like in example #2?

What I'm trying to achieve is a class that has instances containing varying sized arrays (but fixed at compile time), that can be populated through a constructor.

I'm not married to using va_list, it just allowed me to fill the varying length arrays. I am however, fixed on being able to fill the arrays through the constructor:

#include <stdarg.h>

class Foo {
  public:
    byte size;
    byte* arr;

    Foo(byte size, ...) : size(size), arr(new byte [size]) {
      va_list arguments;
      va_start(arguments, size);
      for (byte i = 0; i < size; i++)
        arr[i] = va_arg(arguments, byte*);
      va_end(arguments);
    }

    void print() {
      for (byte i = 0; i < size; i++)
        Serial.print((String)arr[i] + ", ");
      Serial.println();
    }
};

void setup() {
  Serial.begin(115200);
  static Foo fooOne(2, 1, 2 );
  static Foo fooTwo(4, 10, 9, 8, 7);
  static Foo fooThree(6, 20, 21, 22, 23, 24, 25);
  fooOne.print();
  fooTwo.print();
  fooThree.print();
}

void loop() {
  // put your main code here, to run repeatedly:
}

Output:

1, 2, 
10, 9, 8, 7, 
20, 21, 22, 23, 24, 25, 

Is there a way to achieve the above without va_list ? or by using template<typename ... Args> instead?

Again, I'm still missing it. Code #1 prints "3" without the "leading argument". Isn't that what you want?

It achieves the counting part yes, but as I hopefully explained above, when I want to use the count to resize the arrays and populate them with a loop using va_list I hit the errors I mentioned.

If I'm being very silly, please do enlighten me!

Could you modify the code in post #5 to strip off the "leading argument" like in post #1? Because as in my other posts above, all my attempts have failed so far.

Well, it might be kind of hacky .... and it has absolutely no type checking or even confirming that 'new' was able to allocate the memory or even if an in-range argument is supplied to getElement() ... those are left as an exercise to the read ... but it seems to work:

class Foo {
  public:
    ~Foo() {
      delete[] arr;
    }

    template<class... Others>
    Foo(Others ... others) {
      allocate(others...);
    }

    Foo() {
    }

    uint8_t getArgCount() {
      return argCount;
    }

    uint8_t getElement(uint8_t i) {
      return arr[i];
    }

  private:
    uint8_t *arr;
    uint8_t count = 0;
    uint8_t argCount = 0;

    void allocate() {
      arr = new uint8_t[count];
      argCount = count;
    }

    template<class T, class... Others>
    void allocate(T t, Others ... others) {
      uint8_t lowerByte = t & 0xFF;
      count++;
      allocate(others...);
      count--;
      arr[count] = lowerByte;
    }
};

Foo foo3Arg(1, 2, 3, 5, 6, 7, 8, 9, 10);

void setup() {
  Serial.begin(115200);
  delay(1000);

  uint8_t argCount = foo3Arg.getArgCount();
  for (uint8_t i = 0; i < argCount; i++) {
    Serial.printf("[%d]: %d\n", i, foo3Arg.getElement(i));
  }
}

void loop() {
}

:joy: That's everything I've ever written. 'Fake it 'till you make it is' how I roll in C++.

I'll do some digging about and play around with your version to see if I can use it. I appreciate you taking the time to write it up!

Also, I assume (as your example is quite different from mine) that how I was trying to run at the problem would not have worked.

Simplified a little bit. Got rid of one instance variable.

class Foo {
  public:
    ~Foo() {
      delete[] arr;
    }

    template<class... Others>
    Foo(Others ... others) {
      allocate(others...);
    }

    Foo() {
    }

    uint8_t getArgCount() {
      return argCount;
    }

    uint8_t getElement(uint8_t i) {
      return arr[i];
    }

  private:
    uint8_t *arr;
    uint8_t argCount = 0;

    void allocate() {
      arr = new uint8_t[argCount];
    }

    template<class T, class... Others>
    void allocate(T t, Others ... others) {
      uint8_t lowerByte = t & 0xFF;
      uint8_t index = argCount;
      argCount++;
      allocate(others...);
      arr[index] = lowerByte;
    }
};

Foo foo10Arg(1, 2, 3, 5, 6, 7, 8, 9, 10);

void setup() {
  Serial.begin(115200);
  delay(1000);

  uint8_t argCount = foo10Arg.getArgCount();
  for (uint8_t i = 0; i < argCount; i++) {
    Serial.printf("[%d]: %d\n", i, foo10Arg.getElement(i));
  }
}

void loop() {
}

@gfvalvo don't see this as me ignoring your wicked contribution... I merely had a random idea about using the va_list twice and using a while loop to count:

#include <stdarg.h>

class Foo {
  public:
  byte* arr = nullptr;
  byte size = 0;
  
    Foo(...) {
      va_list arguments; // Initialise the va_list

      // One pass on the va_list to count the arguments
      va_start(arguments, 0);
      while (va_arg(arguments, byte*))
        size++;
      va_end(arguments);

      Serial.println((String)"Num Args: " + size);

      // Resize the array using the argument count
      arr = new byte [size];

      // A second pass to fill the newly sized array with the arguments
      va_start(arguments, size);
      for (byte i = 0; i < size; i++)
        arr[i] = va_arg(arguments, byte*);
      va_end(arguments);

      // Print the array
      print();
    }

    ~Foo(){
      delete[] arr;
      Serial.println("Deleted");
    }

    void print() {
      for (byte i = 0; i < size; i++)
        Serial.print((String)arr[i] + ", ");
      Serial.println();
    }
};

void setup() {
  Serial.begin(115200);
  Foo fooOne(1, 2, 3, 4, 5, NULL);
  Foo fooTwo(10, 11, 12, 13, 14, 15, NULL);
  Foo fooThree(4, 5, 6, NULL);
}

void loop() { }

Output:

Num Args: 5
1, 2, 3, 4, 5, 
Num Args: 6
10, 11, 12, 13, 14, 15, 
Num Args: 3
4, 5, 6, 

What concerns me is the seeking for NULL. I'll be honest, I'm flying by the seat of my pants here and pretty much throwing s*** at the wall until it sticks, so I don't quite understand how va_list works.
Does it initialise some kind of array and fill it with the function arguments? In which case, my NULL check 'could' be okay.

If it's looking for NULL elsewhere in memory then I'm concerned what happens if we run into initialised memory that's being used elsewhere. In which case, this is only working by luck.

I based my code on this example by @PieterP who is far more knowledgeable in these things.

Ah thanks, I'll have a read there too. Is @PieterP still active on the forum? If so, I hope he/she wouldn't mind dropping in to tell me how poorly written and unsafe my example is :smiley:

This: Re-using a 'va_list' - C / C++ suggests maybe re-using the va_list twice is at least okay. Just that NULL check now I guess.

Edit: This: c++ - How to determine if va_list is empty - Stack Overflow also seems to indicate that va_arg(vl, type) will return '0' for numeric types, NULL for pointers and 'zeroed' structs for structs.

So maybe this is actually 'okay' to use. Gah! I wish I just knew more stuff and could be confident with what I wrote.

My coding journey is 95% googling, and 5% battling compiler errors until the thing I wrote stops breaking. Even then who knows what demon bugs I'm hiding for myself later.

Variadic templates (or parameter packs) are completely separate from C-style variadic functions. C-style variadic functions should never be used in C++. They cannot be checked at compile time, not even the number of arguments or their types, and the implicit conversion and promotion rules are too complicated.

// C-style variadic function, don't use:
void foo(...);
// variadic template, okay, number and type are checked:
template <class... Args>
void bar(Args... args); 

The va_list, va_start and other types and macros from stdarg.h should not be used with variadic templates (or IMHO should not be used at all).

If this works, then it's purely by luck.

AFAIK (and my compiler warnings agree), you are not allowed to pass 0 to va_start. It should be the last regular argument (which must exist, because you must pass some information to the function to let it know what's actually in the variadic arguments).
You also cannot call va_start twice on the same va_list, you have to use va_copy. (But again, you shouldn't use them.)
You're also reading back the arguments as the wrong type (byte * vs int) which is undefined behavior. Variadic arguments of type byte are impossible, they are always promoted to int.

It is also impossible to pass the integer 0 to your function, because it would be interpreted as the sentinel. (NULL can be just the integer 0, which is why you should never use it in C++, use nullptr instead.)

See va_arg - cppreference.com to know what's allowed and what's not.
And then forget variadic argument functions exist.


The variadic templates are fine, because the number of arguments and their types are checked by the compiler, and you're much less likely to shoot yourself in the foot.

If I understand correctly, this should do what you describe:

class Foo {
  public:
    template <class... Args>
    Foo(Args... args)
        : arr{new uint8_t[] { static_cast<uint8_t>(args)... }},
          argCount{sizeof...(args)} {}

    ~Foo() { delete[] arr; }

    uint8_t size() const { return argCount; }
    uint8_t &operator[](size_t i) { return arr[i]; }
    uint8_t operator[](size_t i) const { return arr[i]; }
    uint8_t *begin() { return arr; }
    const uint8_t *begin() const { return arr; }
    uint8_t *end() { return arr + argCount; }
    const uint8_t *end() const { return arr + argCount; }

  private:
    uint8_t *arr;
    uint8_t argCount;
};

Foo foo10Arg(1, 2, 3, 5, 6, 7, 8, 9, 10);

The problem here is the need for the explicit static_cast. Integer literals like 1 are of type int, and initializing an array of bytes with an argument of type int would be a narrowing conversion. So unless you explicitly cast all literals to bytes before calling the constructor, the static_cast is required, but it's too broad of course, you could pass the value 9999 which would overflow without any warnings, which might not be what you expect.

Either way, I don't think this is a good use case for a variadic template, I think an array argument or an initializer list would be more appropriate:

class Foo2 {
  public:
    template <size_t N>
    Foo2(const uint8_t (&args)[N]) : argCount(N) {
        static_assert(N <= 255, "Too many values");
        arr = new uint8_t[N];
        memcpy(arr, args, sizeof(args));
    }

    ~Foo2() { delete[] arr; }

    // ...

  private:
    uint8_t *arr;
    uint8_t argCount;
};

Foo2 foo10Arg({1, 2, 3, 5, 6, 7, 8, 9, 10});
Foo2 foo3Arg{{1, 2, 3}};

Or:

#include <initializer_list> // std::initializer_list
#include <algorithm>        // std::copy

class Foo3 {
  public:
    Foo3(std::initializer_list<uint8_t> args) : argCount(args.size()) {
        arr = new uint8_t[args.size()];
        std::copy(args.begin(), args.end(), arr);
    }

    ~Foo3() { delete[] arr; }

    // ...

  private:
    uint8_t *arr;
    uint8_t argCount;
};

Foo3 foo10Arg({1, 2, 3, 5, 6, 7, 8, 9, 10});
Foo3 foo3Arg{1, 2, 3};

But at that point, you're just reinventing a worse version of std::vector ...

It might be useful to follow a more structured guide, such as https://www.learncpp.com/.

If you're just guessing and trying things out in languages like C++, you'll definitely end up with code that seems to work, but is actually full of Undefined behavior - cppreference.com, which IMO is much worse than code that doesn't work.

I don't know where the guy on StackOverflow got that idea, but my copy of the C11 standard says:

The va_arg macro expands to an expression that has the specified type and the value of the next argument in the call. The parameter ap shall have been initialized by the va_start or va_copy macro (without an intervening invocation of the va_end macro for the same ap). Each invocation of the va_arg macro modifies ap so that the values of successive arguments are returned in turn. The parameter type shall be a type name specified such that the type of a pointer to an object that has the specified type can be obtained simply by postfixing a * to type. If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:
— one type is a signed integer type, the other type is the corresponding unsigned integer
type, and the value is representable in both types;
— one type is pointer to void and the other is a pointer to a character type.

MASSIVE thanks for writing all that out PieterP! I've learned a lot from your post and clearly, there are better ways to achieve what I want to do which is great news!

I was exaggerating a bit with the 95% / 5% C++ is just tough :smiley: I've learned mostly from YT videos, trial and error, Googling and lurking on the forums.

I really like the look of this one... But when I throw it into a sim like Wokwi it's giving a handful of errors

Build failed!
sketch.ino:19:43: error: no matching function for call to 'Foo2::Foo2(<brace-enclosed initializer list>)'
 Foo2 foo10Arg({1, 2, 3, 5, 6, 7, 8, 9, 10});
                                           ^
sketch.ino:4:5: note: candidate: template<unsigned int N> Foo2::Foo2(const uint8_t (&)[N])
     Foo2(const uint8_t (&args)[N]) : argCount(N) {
     ^~~~
sketch.ino:4:5: note:   template argument deduction/substitution failed:
sketch.ino:19:43: note:   mismatched types 'unsigned char' and 'int'
 Foo2 foo10Arg({1, 2, 3, 5, 6, 7, 8, 9, 10});
                                           ^
sketch.ino:1:7: note: candidate: constexpr Foo2::Foo2(const Foo2&)
 class Foo2 {
       ^~~~
sketch.ino:1:7: note:   no known conversion for argument 1 from '<brace-enclosed initializer list>' to 'const Foo2&'
sketch.ino:20:23: error: no matching function for call to 'Foo2::Foo2(<brace-enclosed initializer list>)'
 Foo2 foo3Arg{{1, 2, 3}};
                       ^
sketch.ino:4:5: note: candidate: template<unsigned int N> Foo2::Foo2(const uint8_t (&)[N])
     Foo2(const uint8_t (&args)[N]) : argCount(N) {
     ^~~~
sketch.ino:4:5: note:   template argument deduction/substitution failed:
sketch.ino:20:23: note:   mismatched types 'unsigned char' and 'int'
 Foo2 foo3Arg{{1, 2, 3}};
                       ^
sketch.ino:1:7: note: candidate: constexpr Foo2::Foo2(const Foo2&)
 class Foo2 {
       ^~~~
sketch.ino:1:7: note:   no known conversion for argument 1 from '<brace-enclosed initializer list>' to 'const Foo2&'
Error during build: exit status 1

Did I miss something?

My bad, you need to specify the size of the array:

    template <class... Args>
    Foo(Args... args)
        : arr{new uint8_t[sizeof...(args)] { static_cast<uint8_t>(args)...}},
          argCount{sizeof...(args)} {}

~~It shouldn't be necessary if your compiler is new enough, but this was a C++11 defect that was fixed only recently, and Arduino is known to use pretty old compilers.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1009r2.pdf~~

Edit: was looking at the wrong class.

I can only assume that the error you're seeing is a bug in GCC, it compiles fine in GCC >=9, Clang and MSVC.

Nope, I think we're up and running!

class Foo {
  public:
    template <class... Args>
    Foo(Args... args)
      : arr{new uint8_t[sizeof...(args)] { static_cast<uint8_t>(args)...}}, argCount(sizeof...(args)) {
      print();
    }
    uint8_t *arr;
    uint8_t argCount;

    void print() {
      for (byte i = 0; i < argCount; i++)
        Serial.print((String)arr[i] + ", ");
      Serial.println();
    }
};



void setup() {
  Serial.begin(115200);
  Foo foo10Arg({1, 2, 3, 5, 6, 7, 8, 9, 10});
  Foo foo3Arg({1, 2, 3});
}

void loop() {}

Output:

1, 2, 3, 5, 6, 7, 8, 9, 10, 
1, 2, 3, 

I reckon this is the solution! Thank you very much!

Turns out it was another language defect, not a GCC bug: Issue 1591: Deducing array bound and element type from initializer list - WG21 CWG Issues

Now I remember why I usually use the latest versions of GCC :slight_smile:

Ah okay, I'll have a read!

One final question, and then I promise I'll leave you in peace :smiley:

How can I use this in conjunction with a const char*?

This code, I would expect to work fine, as it's only another parameter:

class Foo {
  public:
    template <class... Args>
    Foo(const char* name, Args... args)
      : name(name), arr{new uint8_t[sizeof...(args)] { static_cast<uint8_t>(args)...}}, size(sizeof...(args)) {
      print();
    }

    const char* name;
    uint8_t *arr;
    uint8_t size;


    void print() {
      Serial.println(name);
      for (byte i = 0; i < size; i++)
        Serial.print((String)arr[i] + ", ");
      Serial.println();
    }
};



void setup() {
  Serial.begin(115200);
  Foo foo10Arg("Test", {1, 2, 3, 5, 6, 7, 8, 9, 10});
}

void loop() {}
sketch.ino: In function 'void setup()':
sketch.ino:26:52: error: no matching function for call to 'Foo::Foo(const char [5], <brace-enclosed initializer list>)'
   Foo foo10Arg("Test", {1, 2, 3, 5, 6, 7, 8, 9, 10});
                                                    ^
sketch.ino:4:5: note: candidate: Foo::Foo(const char*, Args ...) [with Args = {}]
     Foo(const char* name, Args... args)
     ^~~
sketch.ino:4:5: note:   candidate expects 1 argument, 2 provided
sketch.ino:1:7: note: candidate: constexpr Foo::Foo(const Foo&)
 class Foo {
       ^~~
sketch.ino:1:7: note:   candidate expects 1 argument, 2 provided
sketch.ino:1:7: note: candidate: constexpr Foo::Foo(Foo&&)
sketch.ino:1:7: note:   candidate expects 1 argument, 2 provided
Error during build: exit status 1