You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现带编译期检查的捕获源位置的日志成员函数?

问题描述

我想要实现一个和std::print原型一致的日志成员函数,同时能捕获源位置,示例调用如下:

// 可能输出: "file.cc:22: some format args"
my_logger.info("some {} args", "format");

我需要编译期检查格式字符串,不想用std::string_view把格式错误推迟到运行时处理。这意味着format_string_with_location要对Args...泛化,用来存储std::format_string<Args...>。

我当前的实现代码如下:

template <typename... Args>
struct format_string_with_location {
   consteval format_string_with_location(
       std::format_string<Args...> fmt,
       std::source_location location = std::source_location::current()) noexcept
    : format{fmt},
      location{location}
  {
  }

  std::format_string<Args...> format;
  std::source_location location;
};

template <typename... Args>
format_string_with_location(char const*) -> format_string_with_location<Args...>;

export class logger {
public:
  template<class... Args>
  void info(format_string_with_location<Args...> fmt, Args&&... args)
  {
    std::println(fmt.format, std::forward<Args>(args)...);
  }
};

但调用时编译失败,编译器把格式字符串识别为const char*,无法推导为format_string_with_location类型,报错信息如下:

logging.cxx:28:8: note: candidate template ignored: could not match 'format_string_with_location<Args...>' against 'const char *'
   28 |   void info(format_string_with_location<Args...> fmt, Args&&... args)
      |        ^
logging_test.cxx:124:13: error: no matching member function for call to 'info'
  124 |         log.info("Log me");

我见过把info()改成模板结构体的变通方案,但这是成员函数,该方案不适用。当前使用Clang 19与C23,希望解决方案兼容所有C23合规编译器,也可接受C++26标准特性。请问是否有可行的解决办法,还是只能将类型检查推迟到运行时?


解决方案

问题核心是模板参数推导歧义:编译器无法自动将const char*隐式转换为format_string_with_location<Args...>,因为Args...需要同时从格式字符串和后续参数推导,导致冲突。以下是两种合规的解决方式:

方法1:拆分模板参数消除推导歧义

调整logger::info的模板参数结构,将格式字符串对应的参数组与后续实参组分离,让编译器优先推导格式字符串的类型:

#include <format>
#include <source_location>
#include <iostream>

template <typename... FmtArgs>
struct format_string_with_location {
    consteval format_string_with_location(
        std::format_string<FmtArgs...> fmt,
        std::source_location loc = std::source_location::current()) noexcept
        : format(fmt), location(loc) {}

    std::format_string<FmtArgs...> format;
    std::source_location location;
};

// 修正推导指引:基于std::format_string推导,而非直接用const char*
template <typename... FmtArgs>
format_string_with_location(std::format_string<FmtArgs...>) -> format_string_with_location<FmtArgs...>;

export class logger {
public:
    // 拆分模板参数,让FmtArgs...由格式字符串推导,Args...由后续实参推导
    template <typename... FmtArgs, typename... Args>
    void info(format_string_with_location<FmtArgs...> fmt, Args&&... args)
    {
        std::println("{}:{}: {}", fmt.location.file_name(), fmt.location.line(),
                     std::format(fmt.format, std::forward<Args>(args)...));
    }
};

调用时,编译器会先将const char*转换为std::format_string<FmtArgs...>,再通过推导指引生成format_string_with_location<FmtArgs...>,同时Args...由后续实参独立推导,彻底解决歧义。

方法2:直接利用C++23特性简化实现

无需额外结构体,直接让info函数接收std::format_string和默认源位置参数,天然支持编译期检查:

#include <format>
#include <source_location>
#include <iostream>

export class logger {
public:
    template <typename... Args>
    void info(std::format_string<Args...> fmt,
              Args&&... args,
              std::source_location loc = std::source_location::current())
    {
        std::println("{}:{}: {}", loc.file_name(), loc.line(),
                     std::format(fmt, std::forward<Args>(args)...));
    }
};

这种方式最简洁,完全符合C++23标准,所有合规编译器都支持,同时保留了格式字符串的编译期检查能力。

验证效果

调用代码示例:

int main() {
    logger log;
    log.info("some {} args", "format"); // 正常编译,输出带源位置的日志
    // log.info("some {} args"); // 编译报错:格式参数不匹配(编译期检查生效)
    return 0;
}

输出示例:

main.cpp:15: some format args

内容的提问来源于stack exchange,提问作者chmod222

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 15:18:26