从高层次上讲,我想要实现的是一个具有与 std::print 相同原型的成员日志函数,它还可以捕获源位置,即:

// possible output: "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位置。我认为这是因为模板参数没有以任何方式连接到要推导的const char*参数:

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");

我曾见过函数转变为的,以便该结构的构造函数同时接收格式字符串和 args 参数,帮助编译器推断出正确的类型,但这在这里不起作用,因为它是一个成员函数。info()template <typename... Args> struct info

这是在 Clang 19 上使用 C++23,但可能的解决方案应该可以在所有兼容 C++23 的编译器上编译。如果即将推出一些超级实验性的 C++26 可以帮助解决这个问题,我也可以接受,只要它是标准 C++。

我是不是在与风车作战,应该将类型检查转移到运行时,还是可以通过某种方式解决这个问题?

2

  • 2
    您可能希望format_string_with_location<std::type_identity_t<Args>...> fmt(在logger)禁止类型推断(失败)。


    – 


  • 效果很好,谢谢!如果你想要正确回答这个问题,我会接受的。


    – 


最佳答案
2

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

Argsformat_string_with_location<Args...>应该从和(相同地)推导出来Args&&...

此外Args不能从CTAD推断出const char*

您应该删除不需要的 CTAD,并且可能会使某些论点变得不可推论:

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

只是建议颠倒一下位置type_identity_t

中指定的方式,你会发现我们有:

// [format.fmt.string], class template basic_format_string
template<class charT, class... Args>
  struct basic_format_string;

template<class... Args>
  using format_string = basic_format_string<char, type_identity_t<Args>...>;
template<class... Args>
  using wformat_string = basic_format_string<wchar_t, type_identity_t<Args>...>;

也就是说,format_string<Args...>它本身已经是一个非推导论证。这使得它的用法相当好 – 你不必担心它:

template <class... Args>
void log(format_string<Args...> fmt, Args&&... args); // just works

所以你应该做一些类似的事情(请注意,我也在修复你的构造函数,否则是不可行的):

namespace impl {
    template <class... Args>
    struct format_string_with_location {
        template <class T> requires std::constructible_from<std::format_string<Args...>, T const&>
        consteval format_string_with_location(T const& fmt,
                                              std::source_location loc = std::source_location::current())
            : fmt(fmt), loc(loc) { }

        std::format_string<Args...> fmt;
        std::source_location loc;
    };
}

template <typename... Args>
using format_string_with_location = impl::format_string_with_location<type_identity_t<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)...);
  }
};

如何实际区分这两个名称(您在签名中使用的别名模板和实际实现该名称的类模板)取决于您。

4

  • 1
    我喜欢这个,谢谢你的建议。这会让签名更简洁,不会太过千篇一律。


    – 

  • 您的演示已打印[/app/example.cpp:8][/app/example.cpp:25]期待!


    – 

  • 修复版本:


    – 

  • @MarekR 谢谢。


    –