How to Use Source_Location in a Variadic Template Function

How to use source_location in a variadic template function?

The first form can be made to work, by adding a deduction guide:

template <typename... Ts>
struct debug
{
debug(Ts&&... ts, const std::source_location& loc = std::source_location::current());
};

template <typename... Ts>
debug(Ts&&...) -> debug<Ts...>;

Test:

int main()
{
debug(5, 'A', 3.14f, "foo");
}

DEMO

Using {fmt} & source_location to create variadic-template-based logging function

You can do it with a struct that represents the format string and location:

#include <fmt/core.h>
#include <source_location>

struct format_string {
fmt::string_view str;
std::source_location loc;

format_string(
const char* str,
const std::source_location& loc =
std::source_location::current()) : str(str), loc(loc) {}
};

void vlog(const format_string& format, fmt::format_args args) {
const auto& loc = format.loc;
fmt::print("{}:{}: ", loc.file_name(), loc.line());
fmt::vprint(format.str, args);
}

template <typename... Args>
void log(const format_string& format, Args&&... args) {
vlog(format, fmt::make_format_args(args...));
}

int main() {
log("invalid squishiness: {}", 42);
}

This prints:

./example.cpp:26: invalid squishiness: 42

Godbolt: https://godbolt.org/z/4aMKcW

C++20 std::source_location yield different column numbers between free function and template functions

I file a PR 99672 to GCC Bugzilla. Jakub Jelinek (one of the GCC contributor) reply me:

I think the standard doesn't specify anything about what exactly the
column should be, so using different columns isn't standard violation.

but he still did a patch to fix it.

Combining function parameter pack and default arguments

The solution would be to us C++20's std::source_location:

#include <iostream>
#include <source_location>

template<typename... Targs, auto location = source_location::current()>
auto tprintf(char const* format, Targs const&... args) -> void {
std::cout
<< std::format("{}:{}: ", location.file_name(), location.line())
<< std::format(format, args...);
}

auto main() -> int {
tprintf("hello {}", "world"); // prints "example.cpp:12: hello world"
}

If C++20 is not an option (especially at this current time, compilers don't support source location) then there is a way to do it without macros. You can simply do a little indirection over the compiler built-ins.

You cannot put defaulted arguments after the variadic arguments, but you can put them in the constructor of the first parameter to have the same effect:

#include <cstdio>

struct location_and_format {
constexpr location_and_format(
char const* _format,
char const* _file_name = __builtin_FILE(),
unsigned _line = __builtin_LINE()
) noexcept : format{_format}, file_name{_file_name}, line{_line} {}

char const* format;
char const* file_name;
unsigned line;
};

template<typename... Targs>
void tprintf(location_and_format format, Targs... args) {
printf("%s:%u: ", format.file_name, format.line);
printf(format.format, args...);
}

int main() {
tprintf("hello %s", "world"); // prints example.cpp:22: hello world
}

Live example

Are there any pitfalls to simulate defaulted function parameters using aggregate initialization?

There is at least one pitfall with lifetime (extension):

const std::string& str = "Hello, world!"; create dangling pointer, (No lifetime extension for member).

Following is fine:

void repeat_print(const std::string& str = "Hello, world!", int t = 1) {/*..*/}

int main()
{
repeat_print();
}

but following is not:

struct bar_t {
const std::string& str = "Hello, world!";
int t = 1;
void operator() () const { /*..*/ }
};

int main()
{
bar_t{}();
}

You might fix bar_t to take member by value, but then you would do extra copy in some cases.

Deducing variadic template arguments with default arguments

You cannot use CTAD on some arguments, but not on the others. You could make a similar syntax work though using tag dispatch. You need to move the specification of the log level to the argument list to accomplish this.

Note that in the following example I'm using std::source_location (C++20) to be compiler independent:

enum class LogLevel
{
INFO
};

template<LogLevel logLevel>
struct LogLevelInfoTag {
constexpr operator LogLevel()
{
return logLevel;
}
};

constinit LogLevelInfoTag<LogLevel::INFO> INFO;

template<LogLevel logLevel, typename ...str>
struct log {
log(
LogLevelInfoTag<logLevel>,
str &&...args,
std::source_location location = std::source_location::current()
) {

std::cout << "[" << location.file_name() << "] [" << location.function_name() << "] [" << location.line() << "] ";
((std::cout << args << " "), ...);
std::cout << std::endl;
}
};

template<LogLevel logLevel, typename ...str>
log(LogLevelInfoTag<logLevel>, str &&...args) -> log<logLevel, str ...>;

// log<INFO> inst("THIS DOESN'T WORK", "ASD", "ASD"); // (desired syntax)
log inst(INFO, "THIS DOESN'T WORK", "ASD", "ASD");

std::source_location as non type template parameter

std::source_location is specified as:

  struct source_location {
// ...

private:
uint_least32_t line_; // exposition only
uint_least32_t column_; // exposition only
const char* file_name_; // exposition only
const char* function_name_; // exposition only
};

And the rules for the kinds of types that can be used as non-template template parameters require that a type be structural, which means, from [temp.param]/7, emphasis mine:

A structural type is one of the following:

  • a scalar type, or
  • an lvalue reference type, or
  • a literal class type with the following properties:
    • all base classes and non-static data members are public and non-mutable and
    • the types of all bases classes and non-static data members are structural types or (possibly multi-dimensional) array thereof.

source_location does not have all of its non-static data members public, so it is not structural, so it is not usable as a non-type template parameter.


This part:

template <auto src_loc = std::experimental::source_location::current().file_name()>

does not work because of [temp.arg.nontype]/3:

For a non-type template-parameter of reference or pointer type, or for each non-static data member of reference or pointer type in a non-type template-parameter of class type or subobject thereof, the reference or pointer value shall not refer to or be the address of (respectively):

  • [...],
  • a string literal object ([lex.string]),
  • ...

But what you can do is create your own type, that is structural, that is constructible from source_location. It's just that the strings can't be char const*, they have to own the data. If you look in the examples in P0732, we can build up:

template <typename Char, size_t N>
struct basic_fixed_string { ... };

template <basic_fixed_string S> struct A {};

using T = A<"hello">;

That might be awkward to deal with in this case, so you could also just pick some reasonable max size and go with that.

Macros not expanding properly

It seems that MSVC does not expand __VA_ARGS__ into multiple arguments (see this related question: MSVC doesn't expand __VA_ARGS__ correctly)

This seems to work:

#define GET_MACRO(_1, _2, NAME, ...) NAME
#define XGET_MACRO(args) GET_MACRO args
#define AL_ASSERT(...) XGET_MACRO((__VA_ARGS__, AL_ASSERT_MSG, AL_ASSERT_NO_MSG))(__VA_ARGS__)

And if you are targeting C++20 (and have Visual Studio 2019 version 16.5), you can use __VA_OPT__:

#define AL_ASSERT(x, ...) if (!(x)) { assertDialog(__VA_OPT__( (__VA_ARGS__) , ) __LINE__, __FILE__); __debugbreak(); }

Or you can do it with some functions:

#define AL_ASSERT(x, ...) (([](bool b, const auto*... msg) { \
static_assert(sizeof...(msg) <= 1, "AL_ASSERT passed too many arguments"); \
if (!b) { \
if constexpr (sizeof...(msg) == 1) { \
const char* m{msg...}; \
assertDialog(msg, __LINE__, __FILE__); \
} else { \
assertDialog(__LINE__, __FILE__); \
} \
__debugbreak();
})(x, __VA_ARGS__))

And with version 16.10 you can do it without a macro with std::source_location:

void AL_ASSERT(bool x, const std::source_location location = std::source_location::current()) {
if (!x) {
assertDialog(location.line(), location.file_name());
__debugbreak();
}
}

void AL_ASSERT(bool x, const char* msg, const std::source_location location = std::source_location::current()) {
if (!x) {
assertDialog(msg, location.line(), location.file_name());
__debugbreak();
}
}


Related Topics



Leave a reply



Submit