I’m trying to learn C++23 and thought a good exercise would be writing a sprintf()-like function that —- provided the given format string is a string literal —- does a compile-time check to ensure the number of arguments match the number of placeholders (here just ‘%’ for simplicity) in the format string, then goes on to create and return the formatted string. Ignoring any string formatting for now, I can’t get the compile-time check on the number of arguments vs. placeholders to work.
For example the code below doesn’t end up calling count_percents() on the string literal at compile time, I think because my use of std::to_string() in Sprintf() is not a constant expression (using GCC trunk —-std=c++2b). If I remove the call to std::to_string(), I can mark both functions consteval and get the desired compile-time check, but then by ability to do much else in Sprintf() is severely constrained (for example I can’t use std::to_string() or create/manipulate then return a std::string).
I realize I could get the desired result by making Sprintf into a variadic macro that first checks the number of arguments with a direct call to a consteval count_percents() before then calling a (non-consteval) helper function to do the actual string formatting (as is done here in C++17), but it feels like there must be a better, macro-free way in C++23.
#include <iostream>
template <int N>
constexpr size_t count_percents(const char (&s)[N]) {
size_t count = 0;
for (size_t i = 0; i < N; i++)
if (s[i] == '%') ++count;
return count;
}
template <int N, typename... Args>
constexpr std::string Sprintf(const char (&format)[N], Args&&... args) {
if (count_percents(format) != sizeof...(args))
throw std::invalid_argument("wrong number of placeholders in format string");
return (std::to_string(args) + ...);
}
int main() {
std::cout << Sprintf("%%", 1, 2); // outputs "12", ok
// ideally wouldn't compile (but does - then throws runtime exception)
std::cout << Sprintf("%", 1, 2);
}
I found this answer helpful, but it doesn’t address my use case where I want a compile-time error based on the number of arguments passed to the non-consteval function. (It would work if all compile-time checks I wanted to do depended only on the contents of the format string itself.)
EDIT
Here’s my own answer to my precise question, which allows checking the argument count at compile time, but the chosen answer below is better because it allows inspecting the actual argument types as well.
https://godbolt.org/z/na5769EaP
#include <cstddef>
#include <iostream>
consteval size_t count_percents(const char* s) {
size_t i = 0;
while (*s) if (*s++ == '%') ++i;
return i;
}
template <size_t N>
struct format_string {
const char* str;
consteval format_string(const char* s) : str(s) {
if (N != count_percents(str))
// if this code path is instantiated,
// this 'throw' is a compile-time error
// in this consteval function
throw std::invalid_argument("wrong number of placeholders");
}
};
template <typename... T>
void my_print(format_string<sizeof...(T)> format, T&&... args) {
// if we're here, placeholder count worked
}
int main() {
my_print("foo%%%", 1, 2, 3); // ok
// my_print("foo%", 1, 2, 3); // compile-time error!
}
std::formatdoes it, or how Victor Zverovich's {fmt} library does it?sprintfand alike were desgned to be a front end of dynamicva_listfunctions. Which almost noone remembers. While underlying dynamic functions have their own uses, e.g. for run-time generation of output in format lines where you don't know the exact format beforehand, a dynamic function which emulates static was just an inevitable evil of C. Also, function behaviour bound to be changed by locale setting and even by OS state. Note, in pre-C99 a function declaration didn't have to list all arguments by default.