0

I use std::format in my logging, and it largely works great. However, std::format ignores extra arguments which I do not like. E.g., std::format("x = {}", x, y) compiles, so I want a stricter wrapper.

I came up with this:

#include <format>
#include <string_view>
#include <type_traits>

// Dummy counts characters, not placeholders.
consteval std::size_t countPlaceholdersDummy(const auto formatString) {
    std::size_t count = 0;
    while (formatString[count] != '\0') ++count;
    return count;
}

template <typename... Args>
class strictFormatString {
   public:
    const std::format_string<Args...> formatString;
    consteval strictFormatString(const auto formatString)
        : formatString(formatString) {
        constexpr std::size_t expected = sizeof...(Args);
        constexpr std::size_t actual = countPlaceholdersDummy(formatString);

        static_assert(
            actual == expected,
            "Number of arguments must exactly match number of placeholders");
    }
};

template <typename... Args>
auto strictFormat(strictFormatString<std::type_identity_t<Args>...> fmt,
                  Args&&... args) {
    return std::format(fmt.formatString, std::forward<Args>(args)...);
}

int main() { strictFormat(""); }

However, this fails to compile in MSVC (Godbolt):

<source>(22): error C2131: expression did not evaluate to a constant
<source>(19): note: failure was caused by a read of a variable outside its lifetime
<source>(19): note: see usage of 'formatString'

How to fix this - without resorting to macros? Ideally, strictFormatString should remain in the code because I use it for other things, too (e.g, capture the caller's std::source_location).

1
  • 1
    The formatString parameter is not a compile-time constant. If you change it to const auto (&formatString)[N] then it can be, but then you have the problem of deducing N, which causes a new set of errors. Commented Nov 7 at 0:41

1 Answer 1

2

It seems the fix is surprisingly simple: make countPlaceholdersDummy constexpr, make actual const, and replace static_assert by a (compile-time) throw.

strictFormat(""); compiles; strictFormat("", 1); does not.

#include <format>
#include <string_view>
#include <type_traits>

// Dummy counts characters, not placeholders.
constexpr std::size_t countPlaceholdersDummy(const auto formatString) {
    std::size_t count = 0;
    while (formatString[count] != '\0') ++count;
    return count;
}

template <typename... Args>
class strictFormatString {
   public:
    const std::format_string<Args...> formatString;
    consteval strictFormatString(const auto formatString)
        : formatString(formatString) {
        constexpr std::size_t expected = sizeof...(Args);
        const std::size_t actual = countPlaceholdersDummy(formatString);

        if (actual != expected) {
            throw(
                "Number of arguments must exactly match number of "
                "placeholders");
        }
    }
};

template <typename... Args>
auto strictFormat(strictFormatString<std::type_identity_t<Args>...> fmt,
                  Args&&... args) {
    return std::format(fmt.formatString, std::forward<Args>(args)...);
}

int main() { strictFormat(""); }
Sign up to request clarification or add additional context in comments.

5 Comments

It does not compile with G++ 15.2.0, it chokes on throw(...: expression '<throw-expression>' is not a constant expression.
Good idea, but far from been complete answer and working godbolt.org/z/T6r5zWTTv
Do note that your code uses the dummy implementation of countPlaceholders, which returns the length of the string, hence, 2. Your examples definitely should not compile because you are not passing 2 arguments.
It is funny you call this "my code".
Funny or not, it's a godbolt link that you posted, with a crucial difference to "my" code that I must assume you made and that explains 100% why it does not behave the way you want it to. Yes, your code 😉

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.