C++ `std::source_location` (C++20) 在日志与断言中的应用

哈喽,各位好!今天咱们来聊聊C++20里一个挺有意思的小家伙:std::source_location。这东西虽然个头不大,但用处可不小,尤其是在日志和断言里,简直是提升开发体验的利器。咱们争取用最通俗的方式,把这玩意儿给盘清楚。

一、std::source_location 是个啥?

简单来说,std::source_location 就像一个“位置标签”,它能自动记录下代码在文件里的位置信息。具体来说,它包含了:

  • 文件名 (file_name()): 代码所在的文件名。
  • 函数名 (function_name()): 代码所在的函数名。
  • 行号 (line()): 代码所在的行号。
  • 列号 (column()): 代码所在的列号。(C++23起可用)

有了这些信息,咱们就能更精确地定位问题,不用再吭哧吭哧地翻代码了。

二、std::source_location 怎么用?

这玩意儿用起来超级简单。它有一个默认的构造函数,会“记住”它被调用的位置。通常,我们会把它作为一个可选的参数传递给日志函数或者断言宏。

#include <iostream>
#include <source_location>

void log_message(const std::string& message, const std::source_location& location = std::source_location::current()) {
  std::cout << "File: " << location.file_name() << ":" << location.line()
            << " Function: " << location.function_name() << " Message: " << message << std::endl;
}

int main() {
  log_message("程序开始运行了!"); // 这里会自动记录 main 函数里的位置
  return 0;
}

运行结果大概是这样:

File: main.cpp:9 Function: int main() Message: 程序开始运行了!

看到了吧?文件名、行号、函数名,一目了然。 std::source_location::current() 是一个静态成员函数,它会返回一个 std::source_location 对象,代表调用它的位置。 如果不显式指定 std::source_location,编译器会自动创建一个默认值,记录当前位置。

三、在日志中的应用:让错误无处遁形

在实际项目中,日志是不可或缺的。有了 std::source_location,咱们的日志就变得更智能了。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <chrono>
#include <iomanip>
#include <source_location>

// 一个简单的日志类
class Logger {
public:
    Logger(const std::string& filename) : filename_(filename) {
        log_file_.open(filename_, std::ios::app); // 追加模式
        if (!log_file_.is_open()) {
            std::cerr << "Error opening log file: " << filename_ << std::endl;
        }
    }

    ~Logger() {
        if (log_file_.is_open()) {
            log_file_.close();
        }
    }

    void log(const std::string& message, const std::source_location& location = std::source_location::current()) {
        if (!log_file_.is_open()) return;

        auto now = std::chrono::system_clock::now();
        auto now_c = std::chrono::system_clock::to_time_t(now);
        std::tm now_tm;
    #ifdef _WIN32
        localtime_s(&now_tm, &now_c);
    #else
        localtime_r(&now_c, &now_tm);
    #endif
        std::stringstream ss;
        ss << std::put_time(&now_tm, "%Y-%m-%d %H:%M:%S");

        log_file_ << "[" << ss.str() << "] "
                  << "File: " << location.file_name() << ":" << location.line()
                  << " Function: " << location.function_name() << " Message: " << message << std::endl;
        log_file_.flush(); // 确保日志立即写入文件
    }

private:
    std::string filename_;
    std::ofstream log_file_;
};

void do_something(Logger& logger) {
  logger.log("Doing something...");
  // 模拟一个错误
  try {
    throw std::runtime_error("Something went wrong!");
  } catch (const std::exception& e) {
    logger.log("Exception caught: " + std::string(e.what()));
  }
}

int main() {
  Logger logger("my_log.txt");
  logger.log("程序启动...");
  do_something(logger);
  logger.log("程序结束...");
  return 0;
}

在这个例子中,我们创建了一个简单的 Logger 类,它接收一个文件名作为参数,并将日志写入该文件。 log 函数接收一个消息和一个可选的 std::source_location 参数。

运行这段代码后,my_log.txt 文件里会记录下类似这样的内容:

[2023-10-27 10:00:00] File: main.cpp:61 Function: int main() Message: 程序启动...
[2023-10-27 10:00:00] File: main.cpp:53 Function: void do_something(Logger&) Message: Doing something...
[2023-10-27 10:00:00] File: main.cpp:57 Function: void do_something(Logger&) Message: Exception caught: Something went wrong!
[2023-10-27 10:00:00] File: main.cpp:63 Function: int main() Message: 程序结束...

这样,即使在复杂的代码中,我们也能快速找到错误发生的位置。

四、在断言中的应用:让调试更有效率

断言是调试代码的利器。当断言失败时,程序会停止执行,并打印一条错误消息。有了 std::source_location,我们可以让断言消息更详细。

#include <iostream>
#include <cassert>
#include <source_location>

// 自定义断言宏
#define MY_ASSERT(condition, message) 
  do { 
    if (!(condition)) { 
      std::cerr << "Assertion failed: " << message << " at " 
                << std::source_location::current().file_name() << ":" 
                << std::source_location::current().line() << " in function " 
                << std::source_location::current().function_name() << std::endl; 
      std::abort(); 
    } 
  } while (0)

int divide(int a, int b) {
  MY_ASSERT(b != 0, "除数不能为零!"); // 使用自定义断言
  return a / b;
}

int main() {
  int result = divide(10, 2);
  std::cout << "Result: " << result << std::endl;

  result = divide(5, 0); // 这里会触发断言
  std::cout << "Result: " << result << std::endl; // 这行不会执行
  return 0;
}

divide(5, 0) 被调用时,断言会失败,并打印出类似这样的错误消息:

Assertion failed: 除数不能为零! at main.cpp:32 in function int main()

有了这个消息,我们就能立刻知道断言失败的原因和位置。

五、std::source_location 的优点和局限性

优点:

  • 方便易用: 使用起来非常简单,只需包含头文件并调用 std::source_location::current() 即可。
  • 自动记录位置信息: 无需手动传递文件名和行号,减少了代码冗余。
  • 提高调试效率: 能够快速定位问题,节省调试时间。
  • 编译时信息: 位置信息在编译时确定,运行时没有额外的性能开销。

局限性:

  • C++20 标准: 只能在支持 C++20 的编译器中使用。
  • 只记录调用位置: 只能记录 std::source_location::current() 被调用的位置,无法追踪更深层次的调用链。
  • 宏的限制: 在宏中使用时需要注意,因为 std::source_location::current() 会记录宏展开后的位置,而不是宏定义的位置。 需要使用内联函数避免这个问题。

六、高级用法:自定义属性

除了默认的属性,std::source_location 还允许我们自定义属性,以便记录更多信息。虽然标准库没有提供直接自定义属性的方法,但我们可以通过封装 std::source_location 来实现。

#include <iostream>
#include <source_location>
#include <string>

// 自定义日志级别
enum class LogLevel {
  Debug,
  Info,
  Warning,
  Error
};

// 封装 std::source_location,添加日志级别
class LogSourceLocation {
public:
  LogSourceLocation(LogLevel level, const std::source_location& location = std::source_location::current())
    : level_(level), location_(location) {}

  LogLevel level() const { return level_; }
  const std::source_location& location() const { return location_; }

  std::string to_string() const {
    std::string level_str;
    switch (level_) {
      case LogLevel::Debug: level_str = "Debug"; break;
      case LogLevel::Info: level_str = "Info"; break;
      case LogLevel::Warning: level_str = "Warning"; break;
      case LogLevel::Error: level_str = "Error"; break;
    }

    return "[" + level_str + "] File: " + location_.file_name() + ":" + std::to_string(location_.line()) +
           " Function: " + location_.function_name();
  }

private:
  LogLevel level_;
  std::source_location location_;
};

void log_message(const std::string& message, const LogSourceLocation& location) {
  std::cout << location.to_string() << " Message: " << message << std::endl;
}

int main() {
  log_message("这是一条调试信息", LogSourceLocation(LogLevel::Debug));
  log_message("这是一条错误信息", LogSourceLocation(LogLevel::Error));
  return 0;
}

在这个例子中,我们创建了一个 LogSourceLocation 类,它封装了 std::source_location,并添加了一个 LogLevel 属性。这样,我们就可以在日志中记录日志级别,方便过滤和分析。

七、和其他日志库集成

虽然我们可以自己写日志类,但在实际项目中,我们通常会使用现成的日志库,比如 spdlogglog 等。 std::source_location 可以很容易地集成到这些日志库中。

spdlog 为例,我们可以这样使用:

#include <iostream>
#include <spdlog/spdlog.h>
#include <source_location>

void log_message(const std::string& message, const std::source_location& location = std::source_location::current()) {
  spdlog::info("[{}:{}] [{}] {}", location.file_name(), location.line(), location.function_name(), message);
}

int main() {
  spdlog::set_level(spdlog::level::debug); // 设置日志级别为 debug
  log_message("这是一条调试信息");
  return 0;
}

这段代码会将日志信息以 spdlog 的格式输出,并包含文件名、行号和函数名。

八、代码示例汇总

为了方便大家查阅,这里把上面用到的代码示例汇总一下:

代码示例 描述
基础用法 展示了 std::source_location 的基本用法,如何获取文件名、行号和函数名。
日志应用 创建了一个简单的 Logger 类,使用 std::source_location 记录日志信息,包括时间戳、文件名、行号和函数名。
断言应用 定义了一个自定义的断言宏 MY_ASSERT,使用 std::source_location 在断言失败时打印详细的错误信息,包括文件名、行号和函数名。
自定义属性 封装了 std::source_location,添加了自定义的 LogLevel 属性,以便在日志中记录日志级别。
spdlog 集成 展示了如何将 std::source_location 集成到 spdlog 日志库中,以便在日志信息中包含文件名、行号和函数名。

九、总结

std::source_location 是 C++20 中一个非常实用的工具,它可以帮助我们更方便地记录代码的位置信息,从而提高调试效率。虽然它有一些局限性,但只要合理使用,就能为我们的开发工作带来很大的便利。希望今天的讲解能够帮助大家更好地理解和使用 std::source_location

好啦,今天的分享就到这里。希望大家以后写代码的时候,能想起这个小小的 std::source_location,让它成为你的得力助手! 如果你有什么问题,欢迎随时提问。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注