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