библиотека — диагностика и отладкаlibrary — diagnostics and debugging
Типобезопасный способ узнать файл/строку/имя функции места вызова через параметр функции со значением по умолчанию std::source_location::current() — вместо макросов __FILE__/__LINE__.A type-safe way to get the caller's file/line/function name via a function parameter defaulted to std::source_location::current() — instead of the __FILE__/__LINE__ macros.
std::source_location (заголовок <source_location>) — тип, хранящий информацию о конкретном месте в исходном коде: имя файла (.file_name()), номер строки (.line()), номер столбца (.column()) и имя функции (.function_name()). Получают его через статический метод std::source_location::current(), обычно используемый как значение параметра функции по умолчанию — тогда он автоматически заполняется местом ВЫЗОВА этой функции, а не местом её определения.std::source_location (from <source_location>) is a type holding information about a specific point in the source code: the file name (.file_name()), the line number (.line()), the column number (.column()), and the function name (.function_name()). You obtain it via the static method std::source_location::current(), typically used as a function parameter's default value — in that position it automatically fills in with the location of the CALL to that function, not the location of its definition.
#define LOG(msg) log_impl(msg, __FILE__, __LINE__)
void log_impl(std::string_view msg, const char* file, int line);
LOG("hello");
// работает, но требует макроса вместо обычной функции —// works, but needs a macro instead of an ordinary function —
// теряются перегрузки, пространства имён, типобезопасность вызова// losing overloads, namespaces, call-site type safety
void log(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::cout << loc.file_name() << ":" << loc.line() << ": " << msg;
}
log("hello");
// loc заполнится местом ЭТОГО вызова log() — обычная функция,// loc fills in with THIS call site of log() — an ordinary function,
// никакого макроса, перегрузки и пространства имён работают как обычно// no macro, overloads and namespaces work as usual
Макросы __FILE__/__LINE__/__func__ решали задачу диагностики места вызова с самого начала C, но ценой отказа от обычных функций:The __FILE__/__LINE__/__func__ macros had solved the call-site diagnostics problem since the earliest days of C, but at the cost of giving up ordinary functions:
Эти макросы разворачиваются препроцессором на месте своего появления в тексте — их нельзя передать как значение параметра функции по умолчанию и рассчитывать на разворачивание в месте вызова: если написать void log(const char* file = __FILE__, int line = __LINE__), оба макроса развернутся в место ОПРЕДЕЛЕНИЯ функции (файл, где объявлен log), а не в место её вызова — что почти никогда не то, что нужно для логирования или отладки.These macros are expanded by the preprocessor right where they appear in the text — they can't be passed as a function parameter's default value and expected to expand at the call site: if you write void log(const char* file = __FILE__, int line = __LINE__), both macros expand at the function's DEFINITION site (the file where log is declared), not at its call site — almost never what you want for logging or debugging.
Единственный рабочий обход — обернуть вызов в собственный макрос (#define LOG(msg) log_impl(msg, __FILE__, __LINE__)), который разворачивается непосредственно в точке вызова и потому подставляет правильные __FILE__/__LINE__. Но макрос вместо функции означает потерю пространств имён, перегрузок, шаблонов — и типобезопасности самого макро-вызова (макрос не проверяет типы аргументов на этапе своей подстановки).The only working workaround was to wrap the call in your own macro (#define LOG(msg) log_impl(msg, __FILE__, __LINE__)), which expands directly at the call point and so substitutes the correct __FILE__/__LINE__. But a macro instead of a function means losing namespaces, overloads, templates — and the type safety of the macro invocation itself (a macro doesn't type-check its arguments at substitution time).
Стандарт вводит специальное правило: std::source_location::current(), использованный как значение параметра по умолчанию, вычисляется в точке вызова функции, а не в точке её определения — в отличие от обычных default-аргументов и в отличие от макросов. Это даёт эффект __FILE__/__LINE__ в обычной, типобезопасной, перегружаемой функции — без единого макроса.The standard introduces a special rule: std::source_location::current(), when used as a default parameter value, is evaluated at the function's call site, not at its definition site — unlike ordinary default arguments and unlike macros. This delivers the effect of __FILE__/__LINE__ in an ordinary, type-safe, overloadable function — with no macro at all.
Правило вычисления default-аргументов в C++ обычно таково: выражение по умолчанию подставляется и вычисляется в контексте вызывающего кода, но было бы неверно с точки зрения обычной семантики, если бы current() просто "знал", откуда его вызвали, как обычная функция — на самом деле компилятор обязан специально распознать вызов std::source_location::current() именно в позиции default-аргумента и подставить туда координаты точки вызова функции, к которой этот аргумент относится. Это специальное правило стандарта, а не следствие общей механики компиляции.The usual rule for evaluating default arguments in C++ is that the default expression is substituted and evaluated in the calling code's context — but it would be wrong under ordinary semantics to expect current() to simply "know" where it was called from, like a normal function would. In reality, the compiler is specifically required to recognize a call to std::source_location::current() exactly when it appears in a default-argument position, and substitute the coordinates of the call site of the function that argument belongs to. This is a special standard rule, not a consequence of ordinary compilation mechanics.
#include <source_location>
#include <iostream>
void log(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::cout << loc.file_name() << ":" << loc.line()
<< " in " << loc.function_name() << ": " << msg << "\n";
}
void do_work() {
log("starting"); // loc укажет на эту строку внутри do_work,// loc points at this line inside do_work,
} // а не на строку определения log()// not at log()'s definition line
Тот же механизм часто комбинируют с исключениями — кастомный класс исключения захватывает source_location в момент создания (то есть в точке throw, поскольку конструктор исключения и есть та функция, у которой параметр со значением по умолчанию), и обработчик в catch получает не только текст ошибки, но и точное место, где она была брошена — без ловли исключения сразу на месте броска только ради логирования координат.The same mechanism is often combined with exceptions — a custom exception class captures a source_location at the moment of construction (that is, at the throw site, since the exception's constructor is itself the function with the defaulted parameter), and the handler in catch gets not just the error text but the exact place it was thrown — with no need to catch right at the throw site just to log the coordinates.
class TracedError : public std::runtime_error {
public:
TracedError(std::string_view msg,
std::source_location loc = std::source_location::current())
: std::runtime_error(std::format("{}:{}: {}", loc.file_name(), loc.line(), msg))
, location_(loc) {}
const std::source_location& where() const { return location_; }
private:
std::source_location location_;
};
void validate(int x) {
if (x < 0) throw TracedError("negative value"); // loc — это место throw, а не конструктора// loc is the throw site, not the constructor's
}
try {
validate(-1);
} catch (const TracedError& e) {
std::cerr << "caught at " << e.where().line() << ": " << e.what() << "\n";
}
Несколько вызовов одной и той же функции log() из разных функций программы — каждый вызов автоматически получает свою собственную, отличную от остальных, локацию, без единого изменения самой log().Several calls to the same log() function from different functions in the program — each call automatically gets its own location, distinct from the others, without a single change to log() itself.
void log(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::cout << loc.file_name() << ":" << loc.line() << " — " << msg << "\n";
}
void connect() {
log("connecting..."); // строка внутри connect()// the line inside connect()
}
void disconnect() {
log("disconnecting..."); // другая строка, другая функция — своя локация// a different line, a different function — its own location
}
connect(); // лог укажет на строку внутри connect()// the log points at the line inside connect()
disconnect(); // лог укажет на строку внутри disconnect()// the log points at the line inside disconnect()
Функция проверки условия, которая при нарушении сама печатает файл и строку нарушения — без опоры на макрос assert() и его ограничения (например, невозможность добавить своё сообщение так же гибко).A condition-checking function that, on violation, prints the file and line of the violation itself — without relying on the assert() macro and its limitations (for instance, not being able to attach a custom message as flexibly).
void check(bool condition, std::string_view what,
std::source_location loc = std::source_location::current()) {
if (!condition) {
std::cerr << loc.file_name() << ":" << loc.line()
<< ": check failed: " << what << "\n";
std::abort();
}
}
void set_ratio(double r) {
check(r >= 0.0 && r <= 1.0, "ratio must be in [0, 1]"); // точное место вызова в логе// the exact call site ends up in the log
}
Базовый класс исключений для проекта, автоматически захватывающий место броска в каждом наследнике — без передачи файла/строки вручную в каждом месте, где что-то бросается.A project's base exception class that automatically captures the throw site in every subclass — with no need to pass a file/line manually at every point something gets thrown.
class AppError : public std::exception {
public:
AppError(std::string msg,
std::source_location loc = std::source_location::current())
: msg_(std::move(msg)), loc_(loc) {}
const char* what() const noexcept override { return msg_.c_str(); }
const std::source_location& location() const { return loc_; }
private:
std::string msg_;
std::source_location loc_;
};
throw AppError("config file missing"); // loc_ = эта строка, автоматически// loc_ = this line, automatically
Правило "вычисляется в точке вызова" не зависит от того, шаблонная функция или нет — подстановка места вызова работает совершенно одинаково для обоих случаев, поскольку это свойство самого default-параметра, а не инстанциации шаблона.The "evaluated at the call site" rule doesn't care whether the function is a template — the call-site substitution behaves identically in both cases, since it's a property of the default parameter itself, not of template instantiation.
template <class T>
void trace_value(const T& value,
std::source_location loc = std::source_location::current()) {
std::cout << loc.line() << ": " << value << "\n";
}
trace_value(42); // T = int, loc указывает на эту строку// T = int, loc points at this line
trace_value("hi"sv); // T = string_view, loc указывает на эту строку —// T = string_view, loc points at this line —
// шаблонность функции никак не мешает правилу// the function being a template doesn't affect the rule
Специальное правило "вычисляется в точке вызова" работает только тогда, когда current() стоит именно как значение параметра по умолчанию. Если вызвать std::source_location::current() явно внутри тела функции, он вернёт место ЭТОГО вызова внутри самой функции — то есть её же файл и строку, а не место вызова функции её собственным клиентом. Частая ошибка непонимания механизма — ожидать одинакового поведения в обоих случаях.The special "evaluated at the call site" rule only applies when current() sits exactly as a default parameter value. Calling std::source_location::current() explicitly inside a function body returns the location of THAT call within the function itself — that is, the function's own file and line, not the location where the function's caller invoked it. A common misunderstanding is expecting the same behavior in both cases.
source_location даёт ровно одну точку — обычно непосредственного вызывающего кода, — а не всю цепочку вызовов, приведшую к текущему месту выполнения. Для полноценного stacktrace (весь стек вызовов целиком) в стандарте появится отдельный тип, std::stacktrace, только в C++23 — в C++20 такого механизма нет вовсе.source_location gives exactly one point — typically the immediate caller — not the entire chain of calls that led to the current point of execution. A proper stack trace (the full call stack) gets its own standard type, std::stacktrace, only in C++23 — C++20 has no such mechanism at all.