C++20s std::source_location in action

In today's post, I want to address a question I'm getting occasionally when teaching a C++20 class. From the plenty of new features that we got with C++20, one sticks out as special: std::source_location. Let's dive in to how this element is special and how to use std::source_location best.

std::source_location in a nutshell

The purpose of std::source_location is to provide a macro-free way to obtain source code information like the filename, function name, or line number of an entity found during compilation. You previously used macros like __FUNCTION__ for the same purpose. Which, of course, worked.

The caveat was that if you wanted to pass the function name and the line number, it required two parameters plus usually a trampoline like macro. Below is such an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
A Assert function taking function, line, condition, and  message
void Assert(bool             condition,
            std::string_view msg,
            std::string_view function,
            int              line)
{
  if(not condition) {
    std::clog << function << ':'
              << line  B followed by function and line
              << ": " << msg  C and the message
              << '\n';
  }
}

You can see the Assert function, pure C++ code, but to spare our users cluttering the codebase with __FUNCTION__ and co, the following macro exists as well:

1
2
3
4
5
6
D Macro wrapper to call Assert
#define ASSERT(condition, msg)                                 \
  Assert(condition,                                            \
         msg,                                                  \
         __FUNCTION__,                                         \
         __LINE__)  E Get function and line information  from  caller

Which undoubtedly makes using Assert easier for your users, but could also confuse them because they need to decide now whether to use Assert or ASSERT.

C++20 to the rescue

Adding the capabilities of C++20 to your codebase can lead to a refactoring like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
A Assert function taking condition, message, and source  location
void Assert(bool                 condition,
            std::string_view     msg,
            std::source_location location =
              B current() is special
            std::source_location::current())
{
  if(not condition) {
    std::clog << location.function_name() << ':'
              << location.line() << ": " << msg << '\n';
  }
}

In this Assert implementation you pass a single parameter location of type std::source_location instead of the two parameters for filename and line number. Which I think is already a good first step because the two belong together.

If you take a look at the code below, you can see the real beauty that this implementation brings for your users:

1
2
3
4
5
void Use()
{
  C A call to Assert with  information of Use
  Assert(1 != 2, "Not met");
}

Thanks to the macro-free implementation, there is no additional macro. It is just the function Assert.

The magic of std::source_location

The magic that std::source_location can do is, to obtain the source information from the caller! In all other cases in C++, if you're using a default parameter as I did with location in Assert the information will be obtained at the point of the Assert declaration. But not with std::source_location. The static member function current gets the information from the call-side.

You can read more about std::source_location in my C++20 book Programming with C++20.

How to pass a std::source_location object

One question I got for my C++ Insights YouTube episode C++ Insights - Episode 51: The magic of C++20s std::source_location was, how to pass a std::source_location object? Bu value or by const &?

If you quickly check cppreference you find that they state:

It is intended that std::source_location has a small size and can be copied efficiently.

Which doesn't say much. So let's test things.

A source_location object that fits into a single register would be the best case. We then would be able to pass by value.

Here is a conformance check with Compiler Explorer: compiler-explorer.com/z/jEdrK8z4s

As you can see, libstdc++ (GCC) and libc++ (Clang) manage to create a source_location object with a size equal to a void*. Perfectly fitting into a single register.

Things are different with MSVC STL. The static_assert fails there. A brief look into the implementation from MSVC reveals that they used the naive implementation of source_location where the object stores two uint32_ts and two pointers.

Things are different with libstdc++ and libc++. I stick with libc++, whose implementation you find here. They both store only a single pointer to a special struct. The compiler knows how to generate and fill that struct and hands out a pointer to the data. That way, the source_location object in their implementation fits into a register and can be passed by value.

Conclusion

As sad as the situation makes me, if you're working on a cross-platform project, passing a std::source_location object by const & is the safest option. Sadly, all the work of libstdc++ and libc++ doesn't help here. If you know that you're developing for anything but Microsoft, you can likely pass by value. Why likely? If you're using a special tool chain, it is best to verify the situation yourself.

Andreas

Recent posts