How can we pass a std::source_location as a template argument?

2023 November 15

I want a call to a log function like below to record constant information about the call site, including its source location, in static storage that is initialized before main is entered, not at the call site.

log("format {}", value);

The only technique I know for this is to instantiate a template with a static data member that is initialized with template arguments. In other words, something about this call needs to lead to an instantiation of a template like this:

template <std::source_location loc>
struct event {
static constexpr std::source_location loc_ = loc;
};

Except that std::source_location is not a "structural type" and thus cannot be used as a non-type template parameter (NTTP).

<source>:3:32: error: 'std::source_location' is not a valid type for a template non-type parameter because it is not structural

For now, let's see if we can just get the file name.

template <char const* file_name>
struct event {
static constexpr char const* const file_name_ = file_name;
};

Trying to instantiate this type with a string literal leads to the first sign of trouble:

event<"abc"> ev;
<source>:7:16: error: '"abc"' is not a valid template argument for type 'const char*' because string literals can never be used in this context
7 | event<"abc"> ev;

We run into a similar problem if we try with the file name from std::source_location::current() (which is consteval):

event<std::source_location::current().file_name()> ev;
<source>:9:52: error: '&"<source>"[0]' is not a valid template argument of type 'const char*' because '"<source>"' is not a variable or function
9 | event<std::source_location::current().file_name()> ev;

I've learned that string literals are eplicitly forbidden from being passed as template arguments for pointer type NTTPs[1]. std::source_location::current().file_name() returns an address of a string literal, meaning it is impossible to pass it as a template argument.

But there is a loophole around this rule: if the string literal is implicitly constructing a "literal class type" NTTP. That class type, call it cstring, needs to copy the string into a fixed-size char array that it holds. It could hold a char const*, but that member can never be the address of a string literal, because no NTTP, or any part of an NTTP, can be the address of a string literal. To declare the char array, cstring needs to know the string size, for which it can use either a string literal initializer (which we've established is impossible to pass as a template argument) or an integral constant expression. We can use class template argument deduction (CTAD) to deduce the constant size for a string literal.

template <std::size_t N>
struct cstring {
char data_[N];
constexpr cstring(char const* data) {
std::copy_n(data, N, data_);
}
};

template <std::size_t N>
cstring(char const (&data)[N]) -> cstring<N>;

template <cstring file_name>
struct event {
static constexpr char const* const file_name_ = file_name.data_;
};

event<"abc"> ev;

Unfortunately, this size deduction does not work for std::source_location::file_name(), which is a char const*.

event<std::source_location::current().file_name()> ev;
<source>:8:15: note:   template argument deduction/substitution failed:
<source>:22:54: note: couldn't deduce template parameter 'N'
22 | event<std::source_location::current().file_name()> ev;

Let's just use a large constant for now. I'll explain the trouble with trying to find the exact size later.

template <std::size_t N = 128>
struct cstring {
char data_[N] = {};
constexpr cstring(char const* data) {
auto n = std::min(N, std::char_traits<char>::length(data));
std::copy_n(data, n, data_);
}
};

Great. Now I have a storage type that can capture everything I want. What about the log function that I said I wanted at the start of this post? If we rename event to log, then I can use its constructor as the function.

template <cstring file_name>
struct log {
static constexpr char const* const file_name_ = file_name.data_;
template <typename... Args>
constexpr log(char const* format, Args&&... args) {
std::printf("%s\n", file_name_);
}
};

log<std::source_location::current().file_name()>("format {}", value);

Ok, fine. It's just very ugly and unfriendly to write. Is there a way I can get the file name of the log statement without calling std::source_location::current() in the log statement?

In a moment like this, my first instinct is to reach for a default template argument, but std::source_location::current() as a default template argument returns the location of the default template argument declaration, not the location of the template argument list (i.e. the call site of the log function), and reasonably so.

But forget about strings for a second. I want the line number as an NTTP too, but that runs into the same problem: either an ugly long-winded template argument list at the call site, or a default template argument that has the wrong line number.

I was starting to think there is no way to get what I want without what would essentially be a constexpr parameter: a parameter whose argument must be a constant expression, and can be used in a template argument list, but that enjoys the default argument behavior of a function parameter (i.e. constructed at the call site). Then it struck me: a default argument for the NTTP constructor.

template <std::size_t N = 128>
struct nttp_t {
char file_name_[N + 1] = {};
constexpr nttp_t(std::source_location loc = std::source_location::current()) {
char const* file_name = loc.file_name();
auto n = std::min(N, std::char_traits<char>::length(file_name));
std::copy_n(file_name, n, file_name_);
file_name_[n] = 0;
}
};

log<{}>("format {}", value);

Good enough for now.


Ok, back to the problem with trying to compute the length of the file name. We need the length resolved in the template arguments for nttp_t, but:

  • the return value of source_location::file_name() cannot be passed as a template argument (pointer to string literal), and
  • std::source_location cannot be passed as a template argument (not a structural type), and
  • computing the length in a default template argument, e.g. std::size_t N = std::char_traits<char>::length(std::source_location::current().file_name()), uses the source location of the default template argument, not the call site.

This is the same problem we had with the file name itself. I tried the trick recursively: an NTTP for the NTTP, with a constructor that computes the file name length from its default arguments.

struct length_t {
std::uint_least32_t value_;
constexpr length_t(std::source_location loc = std::source_location::current())
: value_(std::char_traits<char>::length(loc.file_name()))
{}
};

template <length_t length>
struct nttp_t {
static constexpr std::uint_least32_t const N = length.value_;
};

However, this means that the template argument for nttp_t cannot be deduced from the "pretty" syntax of log<{}>("format {}", value).

<source>:36:11: note:   couldn't deduce template parameter 'length'
36 | log<{}>("format {}", value);

Instead, we are forced to write log<nttp_t<{}>{}>("format {}", value). There are two ways to try to get around this, but they both end up with the wrong source location. (It is difficult to demonstrate these failures for the file name in a single source file example in Compiler Explorer, but it becomes clear if we test with the line number, which the next examples do.)

  • If we try to add a default argument for template parameter length, i.e. nttp_t<length_t length = {}>, then the source location comes from the default template argument, not the call site.
  • If we add a CTAD for nttp_t, nttp_t() -> nttp_t<{}>, then it is effectively the same as adding a default template argument, except that the source location comes from the CTAD, not the call site.

This remains an open problem for now.


Thanks to Luke D'Alessandro and Arthur O'Dwyer for helping me out in the C++ Slack.

Footnotes

  1. The reason was explained to me thus: It is unspecified whether "foo" and "foo" represent the same address, implying that it is unspecified whether bar<"foo"> and bar<"foo"> represent the same type, which is a problem. ↩︎