Search code examples
c++c++23

Capturing the current source location and std::format_args for a compile-time checked log function


At a high level, what I want to achieve is a member log function with the same prototype as std::print that also captures the source location i.e.:

// possible output: "file.cc:22: some format args"
my_logger.info("some {} args", "format");

This can be solved quite neatly as shown in this answer to a very similar question.

The problem is that I also want compile-time checked format strings, so I don't want to fall back onto std::string_view to store the format string and punt format errors to the runtime. This means the format_string_with_location has to be generic over Args... in order to store std::format_string<Args...>.

What I came up with so far is the following:

template <typename... Args>
struct format_string_with_location {
   consteval format_string_with_location(
       std::format_string<Args...> fmt,
       std::source_location location = std::source_location::current()) noexcept
    : format{fmt},
      location{location}
  {
  }

  std::format_string<Args...> format;
  std::source_location location;
};

template <typename... Args>
format_string_with_location(char const*) -> format_string_with_location<Args...>;

export class logger {
public:
  template<class... Args>
  void info(format_string_with_location<Args...> fmt, Args&&... args)
  {
    std::println(fmt.format, std::forward<Args>(args)...);
  }
};

Now this fails because at the call site, the compiler sees const char* as the format string and it cannot make the jump to format_string_with_location despite my (attempt at a) deduction guide. I assume this is due to the template arguments not being connected in any way to the to-be-deducted const char* parameter:

logging.cxx:28:8: note: candidate template ignored: could not match 'format_string_with_location<Args...>' against 'const char *'
   28 |   void info(format_string_with_location<Args...> fmt, Args&&... args)
      |        ^
logging_test.cxx:124:13: error: no matching member function for call to 'info'
  124 |         log.info("Log me");

I have seen workarounds that would turn the info() function into a template <typename... Args> struct info so the constructor for that struct receives both the format string and args parameters, helping the compiler deduct the correct type, but this doesn't work here because it's a member function.

This is using C++23 on Clang 19, but a possible solution should compile on all C++23 compliant compilers. If there is some upcoming super-experimental C++26 things that help with this, I'm also fine with that, as long as it's standard C++.

Am I tilting at windmills and should just punt type checking to the runtime, or is this solvable in some way?


Solution

  • With

    export class logger {
    public:
      template<class... Args>
      void info(format_string_with_location<Args...> fmt, Args&&... args);
    };
    

    Args should be deduced (identically) from format_string_with_location<Args...> and Args&&...

    In addition Args cannot be deduced from CTAD from const char*.

    You should removed unneeded CTAD, and you might make some argument non-deducible:

    export class logger {
    public:
      template<class... Args>
      void info(format_string_with_location<std::type_identity_t<Args>...> fmt, Args&&... args)
      {
        std::println(fmt.format, std::forward<Args>(args)...);
      }
    };