Объясните механизм фиксации места вызова и исправьте обёртку так, чтобы в журнале сохранялась строка из main, а не из info.
#include <iostream>
#include <source_location>
#include <string_view>
void log(std::string_view text,
std::source_location loc = std::source_location::current()) {
std::cout << loc.function_name() << ':' << loc.line()
<< " " << text << '
';
}
void info(std::string_view text) {
log(text);
}
int main() {
info("failure");
}
В представленном коде std::source_location::current() срабатывает при подстановке аргумента по умолчанию в вызове log(text), поэтому журнал фиксирует строку внутри info, а не строку вызова info из main.
Чтобы сохранить исходное место вызова, нужно добавить параметр std::source_location со значением по умолчанию в info и явно передать его в log:
До появления std::source_location для добавления файла, строки и имени функции в диагностику обычно применяли макросы __FILE__, __LINE__ и compiler-specific средства. Такие решения плохо сочетались с обёртками: макрос должен был раскрываться непосредственно в месте вызова либо требовал дополнительной макрообёртки.
std::source_location появился в C++20 как типизированный стандартный способ получить координаты исходного кода без передачи их вручную и без макросов. Особенно удобен его параметр со значением по умолчанию в функциях журналирования и диагностики.
Параметр по умолчанию не является заранее вычисленным значением. Если аргумент не передан, выражение std::source_location::current() вычисляется в месте, связанном с вызовом функции, где этот параметр заполняется.
Вызов log(text) находится внутри info, поэтому исходная версия теряет информацию о вызывающем коде. В большой системе это приводит к неверным строкам в логах, затрудняет поиск ошибки и может быть особенно незаметно, если между приложением и журналированием есть несколько обёрток.
std::source_location::current() формирует объект с именем файла, номером строки, номером столбца и именем функции. Вызов без явного аргумента использует значение по умолчанию, а не данные, автоматически протаскиваемые через весь стек вызовов.
Каждый публичный диагностический слой, который должен сохранять своего вызывающего, должен принимать собственный параметр std::source_location со значением по умолчанию. Внутренний вызов обязан передать этот объект явно:
Если debug вызван из main, значение создаётся на строке вызова debug. Затем debug передаёт уже готовое значение в log, поэтому внутренний вызов не заменяет исходную координату.
У такого подхода есть ограничения. Он сохраняет только одну выбранную точку входа: если между приложением и логгером есть несколько функций, параметр нужно последовательно принимать и передавать через каждую значимую обёртку. Если координаты нужны не для диагностики, а для идентификации события в распределённой системе, одной source_location недостаточно: она не содержит идентификатор процесса, потока или трассы.
В библиотеке есть функции info, warning и error, а все сообщения в конечном счёте поступают в один логгер. Вариант с вызовом std::source_location::current() только в логгере прост и не требует изменения API, но показывает строки внутренних обёрток. Макросы могут показывать место вызова, однако загрязняют интерфейс, усложняют шаблонный код и зависят от препроцессора.
Выбранное решение — сделать std::source_location параметром по умолчанию у каждой публичной функции и передавать его дальше явно. Это немного расширяет сигнатуры, зато сохраняет типобезопасность, не требует макросов и корректно работает с обычными функциями, шаблонами и перегрузками. В результате сообщение указывает на место, где пользователь API вызвал error, а не на реализацию логгера.
Дополнительный вопрос: достаточно ли передать std::source_location по значению через несколько функций?
Ответ: да. std::source_location — небольшой объект-значение, содержащий представление координат исходного кода. Передача по значению сохраняет уже полученную точку и не вызывает повторный current(). Важно не объявлять на каждом промежуточном уровне новый параметр со значением по умолчанию, если этот уровень не должен стать новой точкой фиксации.
Дополнительный вопрос: что произойдёт, если вызвать log(text, std::source_location::current()) прямо из info?
Ответ: будет зафиксирована строка явного вызова current(), то есть строка внутри info. Явная передача не восстанавливает место, откуда была вызвана info; для этого info должна получить координату от своего вызывающего через параметр по умолчанию.
Дополнительный вопрос: можно ли безопасно хранить file_name() и function_name() после уничтожения объекта std::source_location?
Ответ: возвращаемые значения имеют тип const char* и указывают на строки с длительным временем жизни, предназначенные для описания исходного кода. Сам объект std::source_location можно уничтожить, но сохранять эти указатели как универсальные данные внешнего происхождения не следует без учёта требований реализации и границ API. Для надёжного владения строками приложение может скопировать их в std::string.