How to use source_location in a variadic template function?

The C++20 feature std::source_location is used to capture information about the context in which a function is called. When I try to use it with a variadic template function, I encountered a problem: I can't see a place to put the source_location parameter.

The following doesn't work because variadic parameters have to be at the end:

// doesn't work
template <typename... Args>
void debug(Args&&... args,
           const std::source_location& loc = std::source_location::current());

The following doesn't work either because the caller will be screwed up by the parameter inserted in between:

// doesn't work either, because ...
template <typename... Args>
void debug(const std::source_location& loc = std::source_location::current(),
           Args&&... args);

// the caller will get confused
debug(42); // error: cannot convert 42 to std::source_location

I was informed in a comment that std::source_location works seamlessly with variadic templates, but I struggle to figure out how. How can I use std::source_location with variadic template functions?


Solution 1:

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

Solution 2:

If your function has a fixed parameter before the variadiac arguments, like a printf format string, you could wrap that parameter in a struct that captures source_location in its constructor:

struct FormatWithLocation {
  const char* value;
  std::source_location loc;

  FormatWithLocation(const char* s,
                     const std::source_location& l = std::source_location::current())
      : value(s), loc(l) {}
};

template <typename... Args>
void debug(FormatWithLocation fmt, Args&&... args) {
  printf("%s:%d] ", fmt.loc.file_name(), fmt.loc.line());
  printf(fmt.value, args...);
}

int main() { debug("hello %s\n", "world"); }

Solution 3:

Just put your arguments in a tuple, no macro needed.

#include <source_location>
#include <tuple>

template <typename... Args>
void debug(
    std::tuple<Args...> args,
    const std::source_location& loc = std::source_location::current())
{
    std::cout 
        << "debug() called from source location "
        << loc.file_name() << ":" << loc.line()  << '\n';
}

And this works*.

Technically you could just write:

template <typename T>
void debug(
    T arg, 
    const std::source_location& loc = std::source_location::current())
{
    std::cout 
        << "debug() called from source location "
        << loc.file_name() << ":" << loc.line()  << '\n';
}

but then you'd probably have to jump through some hoops to get the argument types.


* In the linked-to example, I'm using <experimental/source_location> because that's what compilers accept right now. Also, I added some code for printing the argument tuple.

Solution 4:

template <typename... Args>
void debug(Args&&... args,
           const std::source_location& loc = std::source_location::current());

"works", but requires to specify template arguments as there are non deducible as there are not last:

debug<int>(42);

Demo

Possible (not perfect) alternatives include:

  • use overloads with hard coded limit (old possible way to "handle" variadic):

    // 0 arguments
    void debug(const std::source_location& loc = std::source_location::current());
    
    // 1 argument
    template <typename T0>
    void debug(T0&& t0,
               const std::source_location& loc = std::source_location::current());
    
    // 2 arguments
    template <typename T0, typename T1>
    void debug(T0&& t0, T1&& t1,
               const std::source_location& loc = std::source_location::current());
    
    // ...
    

    Demo

  • to put source_location at first position, without default:

    template <typename... Args>
    void debug(const std::source_location& loc, Args&&... args);
    

    and

    debug(std::source_location::current(), 42);
    

    Demo

  • similarly to overloads, but just use tuple as group

    template <typename Tuple>
    void debug(Tuple&& t,
               const std::source_location& loc = std::source_location::current());
    

    or

    template <typename ... Ts>
    void debug(const std::tuple<Ts...>& t,
               const std::source_location& loc = std::source_location::current());
    

    with usage

    debug(std::make_tuple(42));
    

    Demo

Solution 5:

Not a great solution but... what about place the variadic arguments in a std::tuple?

I mean... something as

template <typename... Args>
void debug (std::tuple<Args...> && t_args,
            std::source_location const & loc = std::source_location::current());

Unfortunately, this way you have to explicitly call std::make_tuple calling it

debug(std::make_tuple(1, 2l, 3ll));