4

I'm trying to count the number of arguments to a function at compile time (I'm wrapping sprintf up in some templates for compile time checks and type safety). I need to check that the number of arguments matches the number of formatting placeholders at compile time. A first pass at this is pretty simple:

template <typename... Args>
constexpr u32
CountArgs(Args&&... args)
{
    return sizeof...(args);
}

constexpr u32
CountFormatSpecifiers(c8* format);

template <typename... Args>
c8*
String_FormatImpl(c8* format, Args&&... args);

#define String_Format(format, ...) \
    String_FormatImpl(format, __VA_ARGS__); \
    static_assert(CountFormatSpecifiers(format) == CountArgs(__VA_ARGS__));

But this breaks down for certain types of arguments. Namely, when passing a reference.

int x = 0;
int& xRef = x;
String_Format("%", xRef);

The compiler complains about CountArgs(__VA_ARGS__) because xRef is not a constant expression. I don't need the value, just the ability to count it. I could wrap it in sizeof or something similar, but that's tough when all I have is __VA_ARGS__ to work with.

Example: https://godbolt.org/z/Diwffy

max66
  • 65,235
  • 10
  • 71
  • 111
Adam
  • 1,122
  • 9
  • 21
  • 3
    Please at least post the compiler error. A minimal reproducible example is always welcomed. – Mikhail Oct 19 '19 at 22:48
  • Added a godbolt link – Adam Oct 19 '19 at 22:51
  • You could place the `static_assert` inside the function that you pass the pack to. Even if the parameters are not `constexpr` the size of the pack is. – super Oct 19 '19 at 23:18
  • `CountFormatSpecifiers(format)` is not constexpr inside the function. – Adam Oct 19 '19 at 23:20
  • In that case, how are you gonna check it's value at compile time? If it's not `constexpr` it can never be used in a `static_assert`. – super Oct 19 '19 at 23:22
  • Oh, I see what you mean. That's not a problem though. Just pass in the return value of `CountFormatSpecifiers(format)` as a template parameter to the same function and use that value to compare against `sizeof...(Args)` – super Oct 19 '19 at 23:26

2 Answers2

2

You can change your macro to something like this

#define String_Format(format, ...) \
    String_FormatImpl<CountFormatSpecifiers(format)>(format, __VA_ARGS__);

template <std::size_t I, typename... Args>
void String_FormatImpl(const char* format, Args&&...) {
    static_assert(I == sizeof...(Args));
    ...
}
super
  • 12,335
  • 2
  • 19
  • 29
  • It's ends up really nasty, but it's workable. The issue is that the template parameters to `String_FormatImpl` are inferred. So that means I'd need to make a wrapper template like `template struct Size {};` to pass the size as an 'argument' and let it get inferred as well. – Adam Oct 19 '19 at 23:34
  • 1
    You can explicitly specify the first parameter and let the rest be inferred. – super Oct 19 '19 at 23:37
  • Holy moly I didn't know that. I think this will work. – Adam Oct 19 '19 at 23:39
  • It is not clear to me why the original code does not work. Please take a look: https://stackoverflow.com/q/58472449/261217 – Mikhail Oct 20 '19 at 11:19
  • @Mikhail From what I can tell the only reason is that the standard currently says so, so the compiler is allowed to do this and still be conformant. – super Oct 20 '19 at 12:59
2

Maybe using decltype() and std::integral_constant ?

I mean... you can declare (only declare: no needs of define it) the following function (EDIT: modified, following a Davis Herring's suggestion (thanks!), to accept const references; this permits to works also with non copyable types)

template <typename ... Args>
std::integral_constant<std::size_t, sizeof...(Args)> CArgs (Args const & ...);

and use it, by example

#define bar(num, ...) \
    static_assert(num == decltype(CArgs(__VA_ARGS__))::value);

This way you don't use the __VA_ARGS__ values in a static_assert() but the type returned by a function that accept __VA_ARGS__.

And the type returned (std::integral_constant<std::size_t, sizeof...(Args)>) contain the number (accessible through ::value), as compile-time constant, of the arguments.

The following is a full compiling example

#include <type_traits>

template <typename ... Args>
std::integral_constant<std::size_t, sizeof...(Args)> CArgs (Args const & ...);

#define bar(num, ...) \
    static_assert(num == decltype(CArgs(__VA_ARGS__))::value);

int main()
{
   int x = 0;
   int& xRef = x;

   //..VV  number of the following arguments    
   bar(3u, x, xRef, 42);
}
max66
  • 65,235
  • 10
  • 71
  • 111