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).
formatStringparameter is not a compile-time constant. If you change it toconst auto (&formatString)[N]then it can be, but then you have the problem of deducingN, which causes a new set of errors.