Search code examples
c++fmt

c++20 Check a String at compile time for substrings


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

}

Solution

  • 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
    }
    

    Live demo