How to use source_location in a variadic template function?
Asked Answered
C

8

73

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?

Corissa answered 18/8, 2019 at 18:23 Comment(16)
Perhaps make debug a macro that will call the real "debug" function with the std::source_location::current() call at the correct argument position (first)?Tiercel
Regarding the removed comments that resulted in the edit: can't we have auto function arguments in templates in c++20?Brainchild
@Someprogrammerdude That will work correctly, but I consider that only a fallback if there's no better method. Using a macro defeats the purpose of std::source_location in some way IMO :(Corissa
@Brainchild Yes, auto is allowed in the parameter, but then we can provide 42 or "foo" as the source location.Corissa
@L.F. Could be an useful customisation point to let the caller use a special function name for example.Brainchild
@Brainchild I didn't intend to do that, but sounds interesting. For the purpose of this question, consider it as a typo :)Corissa
Source location looks constexpr, can something be done by adding it in the template list? (Pointer to?)Atherton
@Atherton It is consteval, actually! But it does not seem specified what happens if it is called as a template parameter.Kraft
@Acorn: en.cppreference.com/w/cpp/experimental/source_location/… still indicates constexprAtherton
@Atherton Look into en.cppreference.com/w/cpp/utility/source_location/current or eel.is/c++draft/support.srcloc#source_location.syn -- I am referring to current()Kraft
So in short, something has to be done at compile time (and that's now forced). Question still holdsAtherton
@L.F.: "Using a macro defeats the purpose of std::source_location in some way IMO" Nonsense. source_location is an actual C++ type and therefore behaves like regular C++ objects that store values. You can pass them around, store their values, and so forth, unlike macros like __LINE__.Vicenta
@NicolBolas I agree with your comment, except that I don’t see how the sentence is nonsense? Because the rest of your comment seems to actually support the nonsense sentence.Corissa
@L.F.: All of the advantages of source_location still exist whether the object is created by a macro or by regular C++ logic. That's my point; it in no way invalidates source_location if you have a DEBUG macro that calls your variadic function with a hand-invoked source_location::current.Vicenta
To followup my own remark, I got something compiling with the only implementation that I could find at CompilerExplorer. It doesn't give the right results. For those interested: godbolt.org/z/5fc4edAtherton
@NicolBolas You are right, being a regular object that can be passed around with its value unchanged is definitely an advantage of source_location. But I’d say the ability to get rid of macros is also an advantage, and that is the purpose I “intended” to defeat. Therefore I agree that the sentence is incomplet, but it is not incorrekt, is it? So it didn’t make much sense to me that it is nonsense. (I don’t know how to produce bad formatting here ...)Corissa
Z
76

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

Zandra answered 18/8, 2019 at 21:24 Comment(4)
Priotr, I don't understand that syntax. Could you explain it a little?Carolyn
@Carolyn It is a deduction guide.Corissa
This seems like a trivial guide. Why does it change the argument deductions?Jarnagin
@Jarnagin Simply put, Ts… are deduced by the deduction guide and hence known by the time overload resolution happens on the constructor, so the constructor can have default arguments.Corissa
S
24

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"); }
Splasher answered 27/2, 2021 at 18:51 Comment(1)
I like this solution (vs the accepted answer with the deduction guide): 1.) it allows you to pass source_location manually if you need to 2.) the function stays a function (and does not become a struct/constructor call) which allows you to add [[ noreturn ]] --> useful if this is supposed to log a fatal errorRivet
M
8

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.

Muna answered 18/8, 2019 at 19:40 Comment(4)
"this works just fine" You mean, besides the fact that you have to put the values in a tuple? And therefore have to deal with a lot of pointless syntax to actually extract and use them for their intended purpose?Vicenta
@NicolBolas: s/a lot of/a bit of/ ; But - see edit.Muna
That all depends on what you're doing with them. In a variadic template, formatting all of the values to a stream is trivial and easily readable. In your version, it is neither. It's doable, but not pretty.Vicenta
@NicolBolas: You might prefer that, but I would say it is just stylistic "problem" to iterate over tuple/variadic template.Quincentenary
Q
5
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

Quincentenary answered 18/8, 2019 at 19:51 Comment(1)
I like your first alternative the best. While it's ugly code, it's the most convenient to use, and that's what's most important.Muna
G
3

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));
Guideboard answered 18/8, 2019 at 18:54 Comment(3)
@L.F. - sorry: maybe I've misunderstood: do you mean that do you want substitute a variadic macro with a template variadic function?Guideboard
My original question doesn’t make sense at all. I have updated my question to make the actual question stand out. Ignore the variadic macros. Sorry!Corissa
@L.F. - I see... well, my answer remain almost the same but the needs of explicitly call std::make_tuple() make it less interesting.Guideboard
B
2

You can try make it:

#include <iostream>
#include <experimental/source_location>

struct log
{
  log(std::experimental::source_location location = std::experimental::source_location::current()) : location { location } {}

  template<typename... Args>
  void operator() (Args... args)
  {
    std::cout << location.function_name() << std::endl;
    std::cout << location.line() << std::endl;
  }

  std::experimental::source_location location;
};

int main() 
{
  log()("asdf");
  log()(1);
}

DEMO

Banta answered 13/3, 2022 at 12:44 Comment(2)
A code-only answer is not high quality. While this code may be useful, you can improve it by saying why it works, how it works, when it should be used, and what its limitations are. Please edit your answer to include explanation and link to relevant documentation.Jacquie
Not the prettiest, but it does work with modules as well. Worth mentioningAground
G
2

If you can accept the use of macros, you can write this to avoid explicitly passing in std::source_ location::current()

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

#define debug(...) debug(std::source_location::current() __VA_OPT__(,) __VA_ARGS__)
Galloway answered 29/8, 2022 at 3:28 Comment(1)
,## is non-standard, and the conforming alternative is __VA_OPT__(,).Whitleywhitlock
S
0

If you want to combine std::format_string with std::source_location, the issue become more complicated to solve. Here is a working code sample that overcomes the drawbacks of templated class constructor deduction guide. The code sample is working over latest GCC, Clang & MSVC with C++23.

#include <format>
#include <iostream>
#include <source_location>

/** @brief A compile-time string format */
struct FormatString : public std::string_view
{
    /** @brief Source location of the string */
    std::source_location sourceLocation;

    /** @brief Constructor */
    template<typename String>
        requires std::constructible_from<std::string_view, String>
    consteval FormatString(const String &string, const std::source_location sourceLocation_ = std::source_location::current()) noexcept
        : std::string_view(string), sourceLocation(sourceLocation_) {}

    /** @brief Get format string */
    template<std::formattable<char> ...Args>
    [[nodiscard]] constexpr const std::format_string<Args...> &get(void) const noexcept
    {
        static_assert(sizeof(std::string_view) == sizeof(std::format_string<Args...>), "This implementation is not compatible with compiled STL");
        return reinterpret_cast<const std::format_string<Args...> &>(*this);
    }
};

/** @brief Print an info message containing caller source location to standard output */
template<std::formattable<char> ...Args>
inline void LogInfo(const FormatString &format, Args &&...args) noexcept
{
    std::cout << format.sourceLocation.function_name() << ": " << std::format(format.get<Args...>(), std::forward<Args>(args)...) << std::endl;
}

int main(void)
{
    // Direct (working with constructor deduction guide)
    LogInfo("Empty args");
    LogInfo(std::string_view("1 arg {}"), 42);
    LogInfo(std::string_view("2 args {} {}"), 42, "hello");

    // Indirect (not working using constructor deduction guide method)
    constexpr std::string_view CompileTimeString = "Hello";
    LogInfo(CompileTimeString);

    return 0;
}
Slowmoving answered 27/5 at 16:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.