80

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?

16
  • 2
    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)? Commented Aug 18, 2019 at 18:34
  • Regarding the removed comments that resulted in the edit: can't we have auto function arguments in templates in c++20? Commented Aug 18, 2019 at 18:35
  • 1
    @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 :( Commented Aug 18, 2019 at 18:37
  • @eerorika Yes, auto is allowed in the parameter, but then we can provide 42 or "foo" as the source location. Commented Aug 18, 2019 at 18:38
  • 1
    @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 ...) Commented Aug 18, 2019 at 21:13

8 Answers 8

84

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

Sign up to request clarification or add additional context in comments.

4 Comments

Priotr, I don't understand that syntax. Could you explain it a little?
@Silicomancer It is a deduction guide.
This seems like a trivial guide. Why does it change the argument deductions?
@KitsuneYMG 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.
37

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

1 Comment

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 error
9

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.

4 Comments

"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?
@NicolBolas: s/a lot of/a bit of/ ; But - see edit.
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.
@NicolBolas: You might prefer that, but I would say it is just stylistic "problem" to iterate over tuple/variadic template.
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

1 Comment

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

3 Comments

@L.F. - sorry: maybe I've misunderstood: do you mean that do you want substitute a variadic macro with a template variadic function?
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!
@L.F. - I see... well, my answer remain almost the same but the needs of explicitly call std::make_tuple() make it less interesting.
3

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__)

Comments

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

2 Comments

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.
Not the prettiest, but it does work with modules as well. Worth mentioning
1

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

1 Comment

How do you make this work with custom formatters? godbolt.org/z/sYbhe6nTr

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.