Today on the std-proposals mailing list, I posted what I understand to be the current best-practice idiom for using C++2a’s std::source_location . I’m repeating it here, so that I have something to link people to when they ask.

There is basically only one reason real code wants the “source location” (file and line number): it’s going to print it out in a programmer-readable log message.

You might say, “What about capturing it into an exception that I’m about to throw?” In that case, what you want is not a single source location but a stack trace. C++2a won’t be able to help you there. You must keep using your existing idiom, which is likely based on backtrace(3) .

First, here’s the “old” (C++03 through forever) idiom that we’re trying to replace. (Godbolt.)

struct OldDebugStream { template<class T> OldDebugStream& operator<<(T msg) { std::cout << msg; return *this; } }; OldDebugStream ods_; #define ods ods_ << __FILE__ << ":" << __LINE__ << ":" int main() { ods << "Hello world! " << 42 << "

"; }

Easy peasy lemon squeezy. Everyone does this today. You might be familiar with this idiom from Google Glog or Boost.Log.

LOG(ERROR) << "Hello world" << 42; // Glog BOOST_LOG_TRIVIAL(error) << "Hello world" << 42; // Boost.Log

Mind you, their macros also do useful things besides capture __FILE__ and __LINE__ ; for example, they can prevent codegen entirely for log messages below a certain compile-time-configured level.

The new idiom

First, you have to know that the way to get the current source location is to write std::source_location::current() . That’s a lot of typing — even more than __FILE__ and __LINE__ ! You could (but this is a bad idea) merely “update” your macro to

#define ods ods_ << std::source_location::current().file_name() << ":" \ << std::source_location::current().line() << ":"

But you’re trying to get rid of the macro altogether. (Why? I don’t know. Personally, I like macros.) So the next thing to know is that source_location::current() is magic in C++2a such that if you put it in a defaulted function parameter, it’ll be evaluated in the caller’s context rather than at the point where the expression syntactically appears in the code. This is novel as far as I know. I don’t know how to get any other function to behave this way. Notably, if you wrap source_location::current() in a helper function, it loses this behavior — even if the helper function is consteval ! (Godbolt.)

So we need a function that can take a defaulted function parameter. But the only functions we ever called in our old idiom were named operator<< , and operator<< can’t take defaulted parameters! How can we insert a new function call, without making it look like a function call at the call-site?

Answer: We can insert a new implicit conversion. An implicit conversion from NewDebugStream to NewDebugStream::Annotated will call the constructor of NewDebugStream::Annotated , and that constructor can take defaulted parameters. Let’s see it in action (Godbolt):

using SourceLoc = std::source_location; struct NewDebugStream { struct Annotated { /*IMPLICIT*/ Annotated(NewDebugStream& s, SourceLoc loc = SourceLoc::current()) { *this << loc.file_name() << ":" << loc.line() << ":"; } }; template<class T> friend Annotated operator<<(Annotated a, T msg) { std::cout << msg; return a; } }; NewDebugStream nds; int main() { nds << "Hello world! " << 42 << "

"; }