CodeGym /Kurslar /C++ SELF /std::source_location: loglara file:line əlavə edirik

std::source_location: loglara file:line əlavə edirik

C++ SELF
Səviyyə, Dərs
Mövcuddur

1. Giriş

Əgər siz 50 sətrdən uzun bir proqram yazmısınızsa, artıq belə bir məqamla rastlaşmısınız: «bir şey səhv getdi», amma logda qürurla yazılıb Error: cannot parse input. Vəssalam. Sanki səhv özü sizə kodun hansı yerində yarandığını, kimlərin valideynləri olduğunu və ümumiyyətlə niyə mövcud olduğunu izah etməlidir.

Kod kiçik olanda, vəziyyəti tez «monitorun üzərinə barmaqla» tapmaq olur. Amma bir neçə funksiya ortaya çıxanda (və adətən çıxır), siz harada xətanın olduğunu təxmin etməyə başlayırsınız: bu arqumentlərin parse edilməsində idi? stdin-in oxunmasında? əmrin işlənməsində? hesablama məntiqində? Nəticədə ya debugger qoşursunuz, ya da əlavə edirsiniz 15 müvəqqəti std::cerr, ya da səssizcə kədərlənirsiniz. Yaxşı loglar kədəri azaldır.

Bu mühazirənin ideyası sadədir: loglara «harada baş verdi» kontekstini file:line (və bəzən function) formatında əlavə etmək, beləcə səhvlərə baxanda siz dərhal yeri görəsiniz.

2. std::source_location nədir

std::source_location standart kitabxanadan kiçik bir tipdir ki, hansısa funksiyanın çağırıldığı kod yerini «söyləyə» bilər. O <source_location> başlıq faylında yerləşir və klassik preprosesor makrolarına — __FILE__, __LINE__, __func__ — normal alternativ olaraq ortaya çıxıb. C++20/23-də bu diaqnostika məqsədləri üçün standartlaşdırılmış alətdir (o cümlədən P1208R6 təklifilə).

std::source_location-un metodları var (adları özləri nə etdiyini göstərir):

  • file_name() — faylın adı,
  • line() — sətir nömrəsi,
  • function_name() — funksiya adı,
  • bəzən əlavə olaraq column() (sütun).

Vacibdir ki, std::source_location icra zamanı sizin .cpp-ni oxumur və mənbələri parslamır. Bu bir detektiv deyil. Bu sadəcə kompilyatorun ona məlum olan «kod yeri» dəyərlərini yerinə qoyma üsuludur.

Minimal nümunə — cari location-u ekrana çıxarmaq:

#include <iostream>
#include <source_location>

void print_location(std::source_location loc = std::source_location::current()) {
    std::cout << loc.file_name() << ':' << loc.line() << '\n';
    // məsələn: main.cpp:42
}

int main() {
    print_location();
}

Diqqət yetirin ki, qəribə amma çox vacib bir hiylə var: loc parametrinin standart dəyəri std::source_location::current().

3. Niyə current() standart dəyər olmalıdır

İndi həmin məqam gəlir ki, burada «ilk implementasiyaların» yarısı qırılır. İntuitiv olaraq istəyir insan belə yazsın: «mən log() daxilində current()-i çağıra bilərəm və harada log etdiyimi alaram».

Problem ondadır ki, yer «siz logu çağırdığınız yer» olmayacaq, əksinə «logger daxilində current() yazılmış yer» olacaq. Yəni bütün xətalar birdən-birə eyni fayldakı eyni sətri göstərəcək — məsələn, logger.cpp içindəki bir sətri (və bu, əlbəttə, çox rahatdır... əgər layihədə bütün səhvlərə logger-i günahlandırmaq istəyirsinizsə).

Buna görə kanonik üsul belədir: std::source_location::current() parametrin standart dəyəri kimi çağırılmalıdır. O zaman «yerin əvəzetmə nöqtəsi» funksiyanın çağırıldığı yer olacaq.

Bu nüans təsadüfi deyil: std::source_location::current ətrafında standartlaşma zamanı formullaşdırma və realizasiya məsələləri müzakirə olunub, çünki «current» məhz düzgün çağırılma yerini ifadə etməlidir.

Pis nümunə (anti-nümunə):

#include <iostream>
#include <source_location>
#include <string_view>

void log_bad(std::string_view msg) {
    auto loc = std::source_location::current(); // həmişə log_bad-in daxilində
    std::cerr << loc.file_name() << ':' << loc.line() << " - " << msg << '\n';
}

Yaxşı nümunə (doğru hadisə nöqtəsi):

#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';
}

İndi log_good("oops") siz yazdığınız log_good(...) sətrini göstərəcək, implementasiyanın daxilindəki sətri deyil.

4. source_location-u logger-ə yerləşdirmək

Əvvəlki mühazirədə bizim minimal logger var idi: səviyyə (info/warn/error), vahid format və adətən çıxış std::cerr. İndi onu genişləndirəcəyik ki, hər yazıya (opsional olaraq) file:line function konteksti əlavə olunsun.

Logger-i 500 sətirlik «kombayn»a çevirməmək üçün diqqətli edəcəyik: source_location-u imzaya əlavə edəcəyik, formatlamanı isə içəridə gizlədəcəyik.

İlk öncə səviyyəmizi xatırladaq:

#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";
}

İndi — source_location-lu log funksiyası:

#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';
}

Bəli, log bir az uzandı. Amma indi hər log(LogLevel::error, "...") dərhal «harada» olduğunu göstərir.

Əgər function_name() qorxuducu görünürsə (bəzən orada imzanı da əhatə edən uzun ad olur), minimalizm ilə başlaya bilərsiniz: yalnız file:line. Bu çox vaxt artıq faydanın təxminən 80% və səs-küyün 20%-i verir.

Mini-versiya:

#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. Kontekst itkisi olmadan log_error/log_warn/log_info bükümləri

Logger bir az rahatlaşdıqda, adətən gözəl funksiyalar yaratmaq istəyirsiniz:

  • log_info("...")
  • log_warn("...")
  • log_error("...")

Və burada ikinci tipik tələyə düşülür: əgər bükümlər source_location-u qəbul etmirsə və içəri ötürmürsə, «hadisə yeri» bükümün daxilindəki sətrə çevrilir. Yəni yenə də source_location-un mənası itir.

Doğru pattern: hər bükümün də loc parametri standart dəyər kimi olsun və onu əsas log-a ötürsün.

Əvvəlcə «əsas» log-u elan edək (faylın yuxarısında ola bilər):

#include <source_location>
#include <string_view>

void log(LogLevel lvl, std::string_view msg, std::source_location loc);

İndi büküm:

#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);
}

eyni info və warn üçün:

#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);
}

Və indi, siz yazdığınız zaman:

log_error("Cannot open file");

log-da log_error(...) çağırış sətri görünəcək, log_error-in daxilindəki sətr deyil.

6. Praktika: mesajlar, CLI və format

std::ostringstream + source_location

Mesajlar çoxaldıqca, std::ostringstream-ə sətirləri + ilə birləşdirmədən yığmağa alışırsınız. Yaxşı xəbər: source_location bununla heç bir şəkildə müdaxilə etmir. Mesajı yığıb sonra log-a göndərə bilərsiniz.

Misal: CLI arqumentlərinin sayını və rejimi loglamaq istəyirik:

#include <sstream>
#include <string>

std::string build_startup_message(int argc) {
    std::ostringstream oss;
    oss << "Program started, argc=" << argc;
    return oss.str();
}

İstifadə:

log_info(build_startup_message(argc));

Müvəqqəti std::string-dən qaçmaq istəsəniz, sonra (hazır olduqda) log-a overload əlavə edib std::string qəbul edə və ya std::string_view qəbul edərək çağırışdan əvvəl string-in ömrünü təmin edə bilərsiniz. Amma bizim səviyyədə sadəcə std::string qaytarmaq daha sadə və aydındır: loglar üçün adətən bu dar yer deyil.

Praktik nümunə: taskbox və hadisə yeri ilə loglar

Gün ərzində biz kiçik bir konsol utilitəsi təxəyyül edirik. Adı taskbox olsun və indilikcə minimal iş görsün: salamlaşma çap etmək, usage göstərmək və interaktiv işləmək. Bugünkü məqsədimiz — istifadəçi qəribə arqumentlər verərsə və ya anlaşılmayan komanda daxil etsə, bizə hadisə yeri ilə aydın diaqnostika verməkdir.

Tutaq ki, usage çap edən funksiyamız var:

#include <iostream>

void print_usage(std::ostream& out) {
    out << "Usage: taskbox [--help] [--interactive]\n";
}

İndi arqumentləri parse edək (çox qısa, sadələşdirilmiş):

#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;
}

Və 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 göstərəcək

    const bool interactive = has_flag(argc, argv, "--interactive");
    if (interactive) {
        log_info("Interactive mode enabled");
        // ... interaktiv dövr
    } else {
        log_info("One-shot mode enabled");
        std::cout << "Hello from taskbox\n"; // Hello from taskbox
    }
}

Məna odur ki, std::cout «nəticə» olaraq qalır, log_* isə diaqnostikanı yazır (adətən std::cerr-ə). Əgər sonra proqramın çıxışını avtomatlaşdırmaq və ya pipe-larla işlətmək istəyirsinizsə, «təmiz stdout» sinirləri xilas edir.

Logun oxunaqlı olması üçün nə qədər kontekst göstərilməlidir

İlk dəfə belə bir log görəndə:

[ERROR] /home/user/project/src/main.cpp:123 int parse_args(...) - Unknown option

siz düşünə bilərsiniz: «Vay, çox detallı». Bu yaxşıdır. Amma kontekst ədviyyat kimidir: çox olarsa, log oxunmaz ola bilər.

Yeni başlayanlar üçün praktik kompromis adətən belədir: həmişə file:line çap etmək, function_name()-i yalnız istəyə görə daxil etmək — məsələn yalnız warn/error üçün, və ya yalnız debug rejimində. Biz mürəkkəb konfiq sistemləri qurmayacağıq, amma ideyanı sadə flag ilə göstərəcəyik.

Məsələn, qlobal konfiqurasiyamız olsun:

struct LoggerConfig {
    bool show_function = false;
};

LoggerConfig g_log_cfg{};

Və onu log-da istifadə edək:

#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';
}

Və bunu CLI flag --log-func ilə yandırmaq olar (istəsəniz), ya da sadəcə inkişaf dövründə sabit olaraq qoymaq. Əsas odur ki, mexanizm aydındır: source_location məlumat verir, siz isə hansını çap edəcəyinizə qərar verirsiniz.

7. std::source_location ilə işləyərkən tipik səhvlər

Səhv №1: std::source_location::current()-i logger daxilində çağırmaq, parametrin standart dəyəri kimi deyil.
Bu ən çox rastlanan problemdir: siz «hadisə nöqtəsini» almaq istəmisiniz, amma «logger daxilindəki nöqtə» alındı. Nəticədə bütün loglar log.cpp-də eyni sətri göstərir və diaqnostika zarafata çevrilir. Həll sadədir: loc = std::source_location::current()-i məhz parametrin standart dəyəri kimi edin.

Səhv №2: rahat bükümlər (log_error()) əlavə edib çağırış yerini itirmək.
Siz log_error("...") əlavə etdiniz və hamısı gözəl göründü, sonra gördünüz ki, bütün xətalar eyni sətrdə baş verir — bükümün daxilində. Deməli, büküm std::source_location-u qəbul edib ötürmür. Düzgün üsul: bükümdə də loc üçün standart parametr olsun və o əsas log-a ötürülsün.

Səhv №3: çox çox kontekst çap etmək və logları oxunmaz etmək.
Bəzən hamısını çap etmək istəyirsən: fayl, sətir, funksiya, sütun, bəlkə əlavə bəzəklər. Praktikada yeni başlayan üçün bu logu elə bir uzun yazıya çevirə bilər ki, mesaj itib. Yaxşı başlanğıc — həmişə file:line, funksiya adını yalnız həqiqi ehtiyac olduqda (və ya yalnız error üçün) daxil etmək.

Səhv №4: proqramın çıxışı ilə diaqnostikanı eyni axında qarışdırmaq.
Əgər logları std::cout-a yazırsınızsa, və proqram da müqavilə üzrə nəticəni std::cout-a yazmalıdırsa, nəticədə istifadəçi nə nəticə, nə xəbərdarlıq olduğunu başa düşə bilmir. Nəticəni std::cout-da, diaqnostikanı isə (xüsusən source_location ilə) std::cerr-də saxlamaq daha məntiqlidir.

Səhv №5: std::source_location-u məcburi alət kimi qəbul etmək.
Bu mövzu inkişaf üçün işarələnir, amma zəriflik üçün yox. Kiçik tədris proqramı bunu olmadan da işləyə bilər. std::source_location-u yalnız o vaxt əlavə etmək məntiqlidir ki, siz artıq «harada loglandığını tapmaq üçün vaxt xərcləyirsiniz» və özünüzü asanlaşdırmaq istəyirsiniz. Bu rahatlıq və intizam alətidir, dini vəzifə yox.

1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Sətlərin delta dəyəri
Sətlərin delta dəyəri
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Yaxşı çağırış
Yaxşı çağırış
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Loq qablaşdırıcısı
Loq qablaşdırıcısı
1
Tapşırıq
C++ SELF, səviyyə, dərs
Bağlanıb
Təmiz çıxış
Təmiz çıxış
1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
CLI args
CLI args
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION