1. Введение
Если вы писали хоть одну программу длиннее 50 строк, вы уже встречали момент: «что-то пошло не так», а в логе гордо написано Error: cannot parse input. И всё. Как будто ошибка сама обязана рассказать вам, в каком месте кода она родилась, кто её родители и почему она вообще существует.
Пока код маленький, можно быстро «пальцем в монитор» найти место. Но как только появляется несколько функций (а у нас они появляются регулярно), вы начинаете тратить время на гадание: это было в парсинге аргументов? в чтении stdin? в обработке команды? в логике вычисления? В итоге вы либо ставите дебаггер, либо добавляете ещё 15 временных std::cerr, либо тихо грустите. Хорошие логи уменьшают грусть.
Идея этой лекции простая: добавить к логам контекст «где это произошло» в формате file:line (и иногда ещё function), чтобы при чтении ошибок вы сразу видели место.
2. Что такое std::source_location
std::source_location — это маленький тип из стандартной библиотеки, который может «подсказать», откуда в коде был вызван лог (или любая другая функция). Он живёт в заголовке <source_location> и появился как нормальная альтернатива классическим препроцессорным макросам __FILE__, __LINE__, __func__. В C++20/23 это уже привычный инструмент, и он стандартизирован именно для задач диагностики (в том числе через предложение P1208R6).
У std::source_location есть методы (названия говорят сами за себя):
- file_name() — имя файла,
- line() — номер строки,
- function_name() — имя функции,
- иногда ещё column() (колонка).
Важно понимать, что std::source_location не читает ваш .cpp «во время выполнения» и не парсит исходники. Это не детектив. Это просто способ, которым компилятор может подставить известные ему значения «места в коде».
Минимальный пример — просто вывести текущую локацию:
#include <iostream>
#include <source_location>
void print_location(std::source_location loc = std::source_location::current()) {
std::cout << loc.file_name() << ':' << loc.line() << '\n';
// например: main.cpp:42
}
int main() {
print_location();
}
Обратите внимание на странный, но очень важный трюк: loc имеет значение по умолчанию std::source_location::current().
3. Почему current() должен быть значением по умолчанию
Сейчас будет момент, на котором ломается половина «первых реализаций» логгера с source_location. Интуитивно хочется написать так: «ну я же внутри log() могу вызвать current() и получить место, где я логирую».
Проблема в том, что место будет не «где вы вызвали лог», а «где внутри логгера написан current()». То есть все ошибки внезапно будут указывать на одну и ту же строку в файле logger.cpp (и это, конечно, очень удобно… если вы хотите обвинить логгер во всех ошибках проекта).
Поэтому канонический приём такой: std::source_location::current() надо вызывать как значение по умолчанию параметра. Тогда «точка подстановки» будет там, где функцию вызвали.
Этот нюанс не случайный: во время стандартизации вокруг std::source_location::current обсуждали формулировки и реализуемость, потому что «current» должен означать именно корректное место вызова.
Плохой вариант (антипример):
#include <iostream>
#include <source_location>
#include <string_view>
void log_bad(std::string_view msg) {
auto loc = std::source_location::current(); // всегда "внутри log_bad"
std::cerr << loc.file_name() << ':' << loc.line() << " - " << msg << '\n';
}
Хороший вариант (правильная точка события):
#include <iostream>
#include <source_location>
#include <string_view>
void log_good(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::cerr << loc.file_name() << ':' << loc.line() << " - " << msg << '\n';
}
Теперь log_good("oops") будет указывать на строку, где вы написали log_good(...), а не на строку внутри реализации.
4. Встраиваем source_location в логгер
На прошлой лекции у нас был минимальный логгер: уровень (info/warn/error), единый формат, и обычно вывод в std::cerr. Сейчас мы расширим его так, чтобы у каждой записи (опционально) появлялся контекст file:line function.
Чтобы не превращать логгер в «комбайн на 500 строк», сделаем аккуратно: добавим source_location в сигнатуру, а форматирование спрячем внутрь.
Сначала напомним наш уровень:
#include <string_view>
enum class LogLevel { info, warn, error };
std::string_view to_string(LogLevel lvl) {
switch (lvl) {
case LogLevel::info: return "INFO";
case LogLevel::warn: return "WARN";
case LogLevel::error: return "ERROR";
}
return "UNKNOWN";
}
Теперь — лог-функция с source_location:
#include <iostream>
#include <source_location>
#include <string_view>
void log(LogLevel lvl,
std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::cerr << '[' << to_string(lvl) << "] "
<< loc.file_name() << ':' << loc.line() << ' '
<< loc.function_name() << " - "
<< msg << '\n';
}
Да, лог стал длиннее. Но зато теперь любой log(LogLevel::error, "...") сразу показывает «где».
Если вам кажется, что function_name() выглядит пугающе (иногда там бывает длинное имя с сигнатурой), можно начать с минимализма: только file:line. Это часто уже 80% пользы и 20% шума.
Мини-версия:
#include <iostream>
#include <source_location>
#include <string_view>
void log(LogLevel lvl,
std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::cerr << '[' << to_string(lvl) << "] "
<< loc.file_name() << ':' << loc.line() << " - "
<< msg << '\n';
}
5. Обёртки log_error/log_warn/log_info без потери локации
Когда логгер становится чуть удобнее, обычно хочется сделать красивые функции:
- log_info("...")
- log_warn("...")
- log_error("...")
И вот тут вторая типичная ловушка: если обёртка не принимает source_location и не пробрасывает его внутрь, то «местом события» станет строка внутри обёртки. То есть вы опять потеряете смысл source_location, только уже более изящно.
Правильный паттерн: каждая обёртка тоже имеет параметр loc со значением по умолчанию, и пробрасывает его в log.
Сначала объявим «основной» log (можно выше по файлу):
#include <source_location>
#include <string_view>
void log(LogLevel lvl, std::string_view msg, std::source_location loc);
Теперь обёртка:
#include <source_location>
#include <string_view>
void log_error(std::string_view msg,
std::source_location loc = std::source_location::current()) {
log(LogLevel::error, msg, loc);
}
То же самое для info и warn:
#include <source_location>
#include <string_view>
void log_info(std::string_view msg,
std::source_location loc = std::source_location::current()) {
log(LogLevel::info, msg, loc);
}
void log_warn(std::string_view msg,
std::source_location loc = std::source_location::current()) {
log(LogLevel::warn, msg, loc);
}
И теперь, когда вы пишете:
log_error("Cannot open file");
в логе будет строка вызова log_error(...), а не строка внутри log_error.
6. Практика: сообщения, CLI и формат
std::ostringstream + source_location
Когда сообщений становится больше, вы уже привыкли к std::ostringstream как к способу собрать строку без конкатенации +. Хорошая новость: source_location не мешает этому вообще. Вы можете собрать сообщение, а потом отправить его в лог.
Пример: хотим залогировать количество аргументов CLI и режим:
#include <sstream>
#include <string>
std::string build_startup_message(int argc) {
std::ostringstream oss;
oss << "Program started, argc=" << argc;
return oss.str();
}
И использование:
log_info(build_startup_message(argc));
Если вы хотите избежать временного std::string, можно (позже, когда будете готовы) добавить перегрузку log под std::string или принимать std::string_view и хранить строку до вызова. Но на нашем уровне проще, честнее и понятнее вернуть std::string: для логов это обычно не узкое место.
Практический пример: taskbox и логи с местом события
В течение дня мы мысленно строим небольшую консольную утилиту. Пусть она называется taskbox и пока умеет минимум: печатать приветствие, показывать usage, и работать в интерактиве. Сегодня наша цель — сделать так, чтобы если пользователь передал странные аргументы или ввёл непонятную команду, мы получили понятную диагностику с местом.
Представим, что у нас есть функция печати usage:
#include <iostream>
void print_usage(std::ostream& out) {
out << "Usage: taskbox [--help] [--interactive]\n";
}
Теперь парсим аргументы (очень коротко, без усложнения):
#include <string_view>
bool has_flag(int argc, char* argv[], std::string_view flag) {
for (int i = 1; i < argc; ++i) {
if (std::string_view{argv[i]} == flag) return true;
}
return false;
}
И в main:
#include <iostream>
int main(int argc, char* argv[]) {
if (has_flag(argc, argv, "--help") || has_flag(argc, argv, "-h")) {
print_usage(std::cout);
return 0;
}
log_info("Starting taskbox"); // покажет file:line
const bool interactive = has_flag(argc, argv, "--interactive");
if (interactive) {
log_info("Interactive mode enabled");
// ... интерактивный цикл
} else {
log_info("One-shot mode enabled");
std::cout << "Hello from taskbox\n"; // Hello from taskbox
}
}
Смысл в том, что std::cout остаётся «результатом», а log_* пишет диагностику (скорее в std::cerr). Если вы потом будете автоматизировать проверку вывода программы или использовать её в пайплайне, «чистый stdout» реально спасает нервы.
Сколько контекста выводить, чтобы лог читался
Когда вы впервые увидите лог вида:
[ERROR] /home/user/project/src/main.cpp:123 int parse_args(...) - Unknown option
вам может показаться: «Ого, подробненько». И это хорошо. Но контекст — как специи: если пересолить, можно сделать лог нечитаемым.
Практический компромисс для начинающих обычно такой: всегда печатать file:line, а function_name() включать по желанию, например только для warn/error, или только в debug-режиме. Мы не будем строить сложную систему конфигов и фильтров, но покажем идею простым флагом.
Например, заведём глобальную настройку:
struct LoggerConfig {
bool show_function = false;
};
LoggerConfig g_log_cfg{};
И используем её в log:
#include <iostream>
#include <source_location>
#include <string_view>
void log(LogLevel lvl,
std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::cerr << '[' << to_string(lvl) << "] "
<< loc.file_name() << ':' << loc.line();
if (g_log_cfg.show_function) {
std::cerr << ' ' << loc.function_name();
}
std::cerr << " - " << msg << '\n';
}
А включение можно сделать хоть через CLI-флаг --log-func (если хочется), хоть просто константой на время разработки. Главное, что механизм понятен: source_location даёт данные, а вы уже решаете, сколько из них печатать.
7. Типичные ошибки при работе с std::source_location
Ошибка №1: вызывать std::source_location::current() внутри логгера, а не в параметре по умолчанию.
Это самая частая проблема: вы искренне хотели получить «точку события», но получили «точку внутри логгера». В результате все логи указывают на одну строку в log.cpp, и диагностика превращается в анекдот. Лечится просто: делайте loc = std::source_location::current() именно значением по умолчанию параметра.
Ошибка №2: сделать удобные обёртки log_error() и потерять место вызова.
Вы добавили log_error("..."), стало красивее, а потом заметили, что все ошибки «происходят» в одной и той же строке внутри log_error. Значит, обёртка не принимает std::source_location и не пробрасывает его. Правильный стиль: и в обёртке параметр loc должен быть со значением по умолчанию, и дальше он передаётся в базовый log.
Ошибка №3: печатать слишком много контекста и сделать логи нечитаемыми.
Иногда хочется вывести всё: файл, строку, функцию, колонку, возможно ещё какие-нибудь «красивости». На практике для новичка это может превратить лог в простыню, где сообщение теряется. Хороший старт — file:line всегда, а имя функции включать только при реальной необходимости (или только для error).
Ошибка №4: смешивать результат работы программы и диагностику в одном потоке.
Если вы печатаете логи в std::cout, а программа по контракту тоже должна печатать результат в std::cout, вы получаете кашу: пользователь не понимает, где результат, где предупреждение. Намного спокойнее держать результат в std::cout, а диагностику (особенно с source_location) — в std::cerr.
Ошибка №5: воспринимать std::source_location как обязательный инструмент.
Эта тема помечается как продвинутая не ради красоты. Маленькая учебная программа может жить и без этого. std::source_location стоит включать тогда, когда вы уже чувствуете, что тратите время на поиск «где именно залогировалось» и хотите облегчить себе жизнь. Это инструмент удобства и дисциплины, а не религиозная обязанность.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ