3

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!
}
3
  • 5
    Have you checked to see how std::format does it, or how Victor Zverovich's {fmt} library does it? Commented Feb 23 at 14:15
  • Make sure to account for this How can I print a '%' character. Commented Feb 23 at 16:36
  • If it's compile time, then it's not sprintf-like function. sprintf and alike were desgned to be a front end of dynamic va_list functions. 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. Commented Feb 23 at 16:51

1 Answer 1

2

Here is modified version of your example that should work: (https://godbolt.org/z/q5T8zr71q)

#include <iostream>
#include <type_traits>

constexpr size_t count_percents(const char* s) {
    size_t count = 0;
    while (*s != '\0') {
        if (*(s++) == '%') {
            count++;
        }
    }
    return count;
}

template <typename... Args>
struct format_args {

    const char* format;

    consteval format_args(const char* format) : format(format) {
        if (count_percents(format) != sizeof...(Args)) {
            throw std::invalid_argument("wrong number of placeholders in format string");
        }
    }
};


template <typename... Args>
constexpr std::string Sprintf(format_args<std::type_identity_t<Args>...> format, Args&&... args) {

    return (std::to_string(args) + ...);

}

int main() {

    std::cout << Sprintf("%%", 1, 2);  // outputs "12", ok

    // now throws correctly at compile time
    std::cout << Sprintf("%", 1, 2); 
}

The answer you've referenced is a good starting point. However, it does not address passing the pack Args to format_args's constructor.

I guess the secret sauce here is the std::type_identity_t. If you'd declare it naively like:

constexpr std::string Sprintf(format_args<Args...> format, Args&&... args) { 
  // impl 
}

... it won't work, because the when the compiler does template resolution, it checks parameters from left to right and try to match them. It will first try to deduce Args... from the type of the first parameter you passed in. Well, it can't because it's a const char*. So it screams out an error!

Here is where type_identity_t comes in handy: it makes the Args pack inside the std::type_identity_t<Args>... live in a non-deduced context, so the compiler won't bother deducing it and will substitute it later if it deduce what Args should be later (which it can from the type of args...).

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

1 Comment

Thanks, this worked perfectly, noting that of course your Sprintf() does not need to be (but of course can be) constexpr. I was able to answer my specific question without type_identity_t (added that answer to my question above) but yours is much better because it allows inspecting the actual argument types for validation, not just checking the count.

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.