3

I'm playing around with std::source_location as a means to define a static identifier for a class which behaves similar to a compile-time counter. The basic idea is rather simple: use a class template taking an integral_constant as template parameter and at the site of instantiation pass the constant expression std::source_location::current().line() to the class.

With the following code using a macro, I can get this behavior without explicitly spelling the std::source_location:

#include <iostream>
#include <source_location>

template<typename, typename index>
struct A_impl
{
    static constexpr auto line() { return index::value; }
};

template<template<typename ...> typename temp, typename index>
struct line_manager
{
    template<typename ... Ts>
    using type = temp<Ts ..., index>;
};

#define A typename line_manager<A_impl, std::integral_constant<int, std::source_location::current().line()> >::type

This can be used as in the following code and allows to separate the two object instantiations at compile time:

int main()
{
    A<double> a; //expands to A_impl<double, integral_constant<int, line()> >
    A<double> b;
    
    if constexpr(a.line() != b.line())
    {
        std::cout<<a.line()<<std::endl;
        std::cout<<b.line()<<std::endl;
    }
}

Demo on Godbolt

Can the same behavior as in the code above -- i.e. logging the source line as a constant expression without explicitly specifying it in the instantiation -- can also be obtained without a macro?

Side note: I know that the above code is not functional, as you could also write A<double> a,b and obtain the same line. That doesn't matter here.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
davidhigh
  • 14,652
  • 2
  • 44
  • 75
  • 2
    I suspect the answer is "no", since you're relying on `std::source_location::current().line()` being evaluated at each point where you define (in your examples) instances of `A`. Not using a macro will tend to cause `std::source_location::current().line()` to be evaluated at the point where you defined the types, not the instances of those types. – Peter Aug 24 '22 at 01:46
  • From [cppreference](https://en.cppreference.com/w/cpp/utility/source_location/current), the only special places where value is **'moved'** are for default member initializer and function default argument (and from standard, it was just a remark)... – Jarod42 Aug 24 '22 at 07:13
  • Currently, type of `a` and `b` differs. storing `std::source_location` as member of `A_Impl` might allow you to pass `current()` in constructor and have the different value as expected... – Jarod42 Aug 24 '22 at 07:17
  • @Jarod42: yes, but as far I can see then it's not a constant expression by definition. The `if constexpr` is crucial here. – davidhigh Aug 24 '22 at 16:35
  • @davidhigh: It is indeed no longer constant expression (unless `a` and `b` are marked `constexpr`) – Jarod42 Aug 24 '22 at 20:43

1 Answers1

2

I come up a way without using macros or direct spelling std::source_location::current().line(), but still needs a bit extra work.

The main idea is to construct a literal type that contains the line number since std::source_location is not literal.

struct Line {
    int line;
    constexpr Line(std::source_location loc = std::source_location::current()): line(loc.line()) {}
};

According to cppref:

If current() is used in a default argument, the return value corresponds to the location of the call to current() at the call site.

So we don't construct a Line until we really need it.

template <typename T, Line L>
struct Logger {
    constexpr static auto line() {
        return L.line;
    }
};

Here Logger is equivalent to A in your case. Then here we go,

Logger<int, {}> x; // x.line() refer to here
Logger<int, {}> y; // y.line() refer to here
std::cout << x.line() << "\n";
std::cout << y.line() << "\n";

Demo on GCC and MSVC

The only extra thing here you need is to pass {} as the second template argument.

(Clang complains, you have to spell Logger<int, Line{}> and you'll get the wrong results. It seems Clang's std::source_location::current().line() implementation doesn't conform the standard. Just let it go...)


You may wonder what if you just use {} as the default argument for Line.

See Demo, GCC works as expected but MSVC refers to the line template <typename T, Line line = {} resides.

BUT I think maybe MSVC conforms more to the standard here.

Nimrod
  • 2,908
  • 9
  • 20
  • *"doesn't conform the standard"*. From [support.srcloc.cons](https://eel.is/c++draft/support.srcloc#cons-1.1), most results are implementation-defined. Not sure how the remark apply to `template`. Especially, with that divergence, type of `x`, `y` might be identical or different :-/ – Jarod42 Aug 24 '22 at 20:59
  • Even though these values are implementation-defined, they shoud be consistent with macros `__LINE__` and `__FILE__`. Take a look at this case https://godbolt.org/z/eqP394h93 Clang's result is apparently unexpected(inconsistent with cases in the remark) unless its macro has an unintuitive value... Also clang can't give the correct function name either. – Nimrod Aug 25 '22 at 02:18
  • *Not sure how the remark apply to template. Especially, with that divergence, type of x, y might be identical or different :-/* The question seems turned into when the expression in templates evaluated. I'm not sure if there's someplace in the standard about this. – Nimrod Aug 25 '22 at 02:43