Most programs produce output. (I suppose a corner-case could be a unit test framework that indicates all tests passing by not throwing an exception, whilst itself not producing any output.) A usual way of confirming a program is correct is by examining its output, this is familiar to programmers as part of the edit-compile-run-debug cycle of coding practice. (Note: I’m not including coding practices such as TDD, which impose a slightly different workflow.)
So, hands up, who’s ever done it? Of course, I refer to inserting debugging code such as std::cerr << "here1\n";
into C++ source files, when you want to ensure a certain code path has been visited. This article intends to show a way for you to never have to use such practices again, thereby avoiding the risk of evidence of debugging hacks remaining in your production code.
The C++20 Standard introduced the sensibly-name header file <source_location>
, which defines struct std::source_location
along with a utility function std::source_location::current()
. This utility function uses hooks into the compiler to populate fields within this struct
which can be read using member functions: line()
; column()
; file_name()
; and function_name()
. It is easy to write a diagnostic function which outputs this information when it is called, for example:
#include <source_location>
#include <iostream>
void log(std::ostream& os = std::cerr, const std::string_view message = {},
const std::source_location location = std::source_location::current())
{
os << location.file_name() << "("
<< location.line() << ":"
<< location.column() << ") `"
<< location.function_name() << "`: "
<< message << '\n';
}
int main() {
log(std::cout, "Modern \"here1\"");
}
Notice that the default assignment to variable location
in the parameter list of log()
is intentional, as it is initialized at the call site. The output from this program is:
location1.cpp(16:5) `main`: Modern "here1"
I advise experimenting with the above program, including calling log()
from member functions, template functions, lambdas etc.
Larger projects invariably involve exception handling, and typically it takes up valuable developer time attempting to discover why such-and-such exception was triggered and exactly where in the code base caused the exception. Using a function such as log()
to output a logging message just before the exception is triggered is useful, but soon we’ll be able to do better, read on.
The upcoming C++23 Standard will include a new header <stacktrace>
, which contains classes related to querying the whole of the function call stack up to where std::stacktrace::current()
is invoked. This utility function usefully returns an object which can be passed straight to a std::ostream
(such as std::cerr
). It is possible to write an exception class which uses this function, again being initialized at the call site (ie. where throw XYZ
occurs), which means that digging around to find where the exception is thrown from is no longer necessary.
The outline of a hypothetical TracedException
class constructor is shown below (this code compiles with current gcc-trunk using -lstdc++_libbacktrace
):
class TracedException {
public:
TracedException(
std::string_view msg = {},
std::stacktrace trc = std::stacktrace::current()) {
if (!msg.empty()) {
std::cerr << msg << '\n';
}
std::cerr << trc;
}
};
This class simply outputs straight to std::cerr
from its constructor, which may be all that is required. An improvement could be deriving from std::exception
and returning a full location and stacktrace string via the what()
member function. A more featured version together with an example program which uses it is shown below:
#include <source_location>
#include <stacktrace>
#include <exception>
#include <string_view>
#include <string>
#include <cstring>
#include <sstream>
#include <iostream>
class LocatedException : public std::exception {
char *msg = nullptr;
public:
LocatedException(bool stacktrace = false,
std::string_view m = {},
const std::source_location loc = std::source_location::current(),
const std::stacktrace trc = std::stacktrace::current()) {
std::ostringstream oss;
oss << loc.file_name() << '(' << loc.line() << ':' << loc.column() << ')';
oss << " in function `" << loc.function_name() << "` " << m << '\n';
if (stacktrace) {
oss << "Stacktrace follows:\n" << trc;
}
msg = new char[oss.str().size() + 1];
strcpy(msg, oss.str().c_str());
}
LocatedException() = delete;
LocatedException(const LocatedException&) = delete;
LocatedException(LocatedException&&) = delete;
const LocatedException& operator=(const LocatedException&) = delete;
const LocatedException& operator=(LocatedException&&) = delete;
virtual const char *what() const noexcept override {
return msg;
}
virtual ~LocatedException() {
delete[] msg;
}
};
void h() {
throw LocatedException(true, "Whoops, shouldn't be here!");
}
void g() {
h();
}
void f() {
g();
}
int main() {
try {
f();
}
catch (std::exception &e) {
std::cerr << "Caught exception: " << e.what() << '\n';
return 1;
}
return 0;
}
Lines 1-8 are the dependencies of this exception class, admittedly there are quite a few. Lines 13-25 are the constructor, the first bool
parameter switching stacktrace output on or off (this could be a global switch, set to false
for production code). We’ve actually used a “string stream” to format the source location and stack trace, this seems to be an easier method. Then the string stream’s C-style string representation is copied to the single data member msg
. Lines 31-33 allow this member to be queried, and lines 34-36 avoid a memory leak. Lines 26-30 are not necessary for this example, but should provide an example of how to make this code production quality. Line 40 triggers the exception, and line 56 is where the actual diagnostic is printed.
The output from this code (as mentioned, compiled using gcc) is:
Caught exception: /app/example.cpp(40:62) in function `void h()` Whoops, shouldn't be here!
Stacktrace follows:
0# h() at /app/example.cpp:40
1# g() at /app/example.cpp:44
2# f() at /app/example.cpp:48
3# at /app/example.cpp:53
4# at :0
5# at :0
6#
This should need no further explanation, note the stacktrace output includes function name, file name and line numbers by default (this will likely be implementation dependent). Look forward to header <stacktrace>
being implemented by other compilers soon, and I highly recommend looking at the examples on cppreference.com.