Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

c++23

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?

like image 777
LukeN Avatar asked Jun 11 '26 17:06

LukeN


1 Answers

Just a suggestion to invert the placement of the type_identity_t.

If you look at the way that this is specified in [format], you'll see that we have:

// [format.fmt.string], class template basic_format_string
template<class charT, class... Args>
  struct basic_format_string;

template<class... Args>
  using format_string = basic_format_string<char, type_identity_t<Args>...>;
template<class... Args>
  using wformat_string = basic_format_string<wchar_t, type_identity_t<Args>...>;

That is, format_string<Args...> is already itself a non-deduced argument. Which makes the usage of it quite nice - you don't have to worry about it:

template <class... Args>
void log(format_string<Args...> fmt, Args&&... args); // just works

So you should do something similar (note that I'm also fixing your constructor, which was otherwise not viable):

namespace impl {
    template <class... Args>
    struct format_string_with_location {
        template <class T> requires std::constructible_from<std::format_string<Args...>, T const&>
        consteval format_string_with_location(T const& fmt,
                                              std::source_location loc = std::source_location::current())
            : fmt(fmt), loc(loc) { }

        std::format_string<Args...> fmt;
        std::source_location loc;
    };
}

template <typename... Args>
using format_string_with_location = impl::format_string_with_location<type_identity_t<Args>...>;

Which then allows the usage you originally wanted:

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)...);
  }
};

How you actually differentiate between the two names (the alias template that you use in your signatures and the class template that actually implements this) is up to you.

Demo.

like image 187
Barry Avatar answered Jun 13 '26 06:06

Barry