I'm looking for a way to check a format string at compile time for invalid characters. I'm using the fmt Lib to format my string.
Unfortunately fmt lib allows to use printf() formatters (like %s, %i) in strings. My Log function may be called from C or C++ context. From C context we have to rely on printf() syntax, but from C++ context we can use python like formatters ({:d} etc.).
I'd like to print a compiler error if old printf() syntax is used from C++ context.
I did came up with this. However I'm getting some compiler errors. Could you help me out getting rid of them?. Thx :)
https://godbolt.org/z/Ko5GhMMPn
Edit: As requested I pasted my code into this question as well
#include <source_location>
#include <string>
#include <string_view>
#include <fmt/format.h>
#include <iostream>
#pragma once
#include <algorithm>
#include <array>
#include <string_view>
using namespace std::string_view_literals;
///some possible printf format args to look for
constexpr auto printfFormatArgs = std::array{ "%s"sv, "%i"sv, "%d"sv };
struct CompileTimeStringCheck
{
std::string_view str;
consteval CompileTimeStringCheck(std::string_view name)
:str(name)
{
if (std::ranges::find(printfFormatArgs, name) != printfFormatArgs.end())
throw;
}
};
inline void checkCompileTimeString(CompileTimeStringCheck name)
{
}
#ifdef __cplusplus
#define LOG_MESSAGE(strLogMessage, ...) log(strLogMessage, ##__VA_ARGS__)
#else
//shown just for completeness => called from C Code
LOG_MESSAGE(strLogMessage, ...) logFromC(strLogMessage, __FILE__, _LINE__, ##__VA_ARGS__)
#endif //#ifdef __cplusplus
void _log(std::string_view formatedMsg, const std::source_location loc = std::source_location::current())
{
//do the actual logging....
//just as dummy
checkCompileTimeString(formatedMsg);
std::cout << formatedMsg<< "loc"<< loc.file_name() <<loc.line() << std::endl;
}
template <typename... Args>
void log(
fmt::format_string<Args...> fmt,
Args&&... args
)
{
std::string strFormatedMessage = fmt::format(fmt, std::forward<Args>(args)...);
_log(std::move(strFormatedMessage));
}
int main()
{
std::string foo = "foo";
LOG_MESSAGE("foo {}",foo); //< Succeed
LOG_MESSAGE("fail %s",foo); //< should show error
LOG_MESSAGE("fail %s"); //< should also fail
}
Since there are quite a few things wrong with your code, I adopted a different tack. I elected to write a simple consteval
function to check that the format string doesn't contain any %
characters (unless immediately followed by another one). I then call this from the LOG_MESSAGE
macro, and then, all being well, call log
.
Here's the code. Note that the format string must be a literal (I think that would be true however you do it):
#include <source_location>
#include <format>
#include <iostream>
consteval bool check_format (const char *fmt)
{
while (*fmt)
{
if (*fmt == '%')
{
if (fmt [1] != '%')
return false;
++fmt;
if (*fmt == 0)
break;
}
++fmt;
}
return true;
}
#define LOG_MESSAGE(fmt, ...) \
static_assert (check_format (fmt)); \
log (std::source_location::current (), fmt, __VA_ARGS__);
template <class... Args>
void log (const std::source_location loc, std::format_string <Args...> fmt, Args&&... args)
{
std::string strFormatedMessage = format (fmt, std::forward<Args>(args)...);
std::cout << strFormatedMessage << " File " << loc.file_name() << ", line " << loc.line() << "\n";
}
int main()
{
std::string foo = "foo";
LOG_MESSAGE("{}", foo); // works
// LOG_MESSAGE("fail %s",foo); // generates compiler error
// LOG_MESSAGE("fail %s"); // ditto, although a different one
}