惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
MyScale Blog
MyScale Blog
WordPress大学
WordPress大学
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
MongoDB | Blog
MongoDB | Blog
V
Vulnerabilities – Threatpost
月光博客
月光博客
罗磊的独立博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
F
Full Disclosure
Simon Willison's Weblog
Simon Willison's Weblog
D
DataBreaches.Net
T
Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
阮一峰的网络日志
阮一峰的网络日志
TaoSecurity Blog
TaoSecurity Blog
Microsoft Azure Blog
Microsoft Azure Blog
Scott Helme
Scott Helme
S
Securelist
W
WeLiveSecurity
K
Kaspersky official blog
The GitHub Blog
The GitHub Blog
Attack and Defense Labs
Attack and Defense Labs
博客园 - 三生石上(FineUI控件)
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
Stack Overflow Blog
Stack Overflow Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
Security Latest
Security Latest
M
MIT News - Artificial intelligence
人人都是产品经理
人人都是产品经理
The Last Watchdog
The Last Watchdog
C
Check Point Blog
T
Troy Hunt's Blog
P
Proofpoint News Feed
J
Java Code Geeks
G
Google Developers Blog
Schneier on Security
Schneier on Security
Cyberwarzone
Cyberwarzone
S
Security @ Cisco Blogs
宝玉的分享
宝玉的分享
Recent Commits to openclaw:main
Recent Commits to openclaw:main
A
About on SuperTechFans
T
The Blog of Author Tim Ferriss
L
LINUX DO - 最新话题
Jina AI
Jina AI

I'm OWenT

国产大模型(GLM 5.1、Kimi K2.6)真实场景效果和 Coding Plan 额度测试 新版本libatapp的连接管理——从etcd服务发现到拓扑驱动的自动重连 新版本libatbus的设计变更——从树形路由到拓扑驱动 Protobuf又一坑 - C++标准和ABI兼容性 AI真好用-给Blog主题统一加mermaid,chart.js,excalidraw,draw.io的多种引入方式支持 给内网部署Squid-通用HTTP下载缓存 UE使用CodeChecker和clang-tidy生成静态分析报告 找出UE的循环依赖 C++小协程栈和临时变量及作用域的栈溢出问题分析 游戏服务的可观测性能力建设(C++生态) 指标上报的多线程优化和多拉取源点优化 协程(libcopp)的Channel功能和CPU命中率优化 通用RPC代码生成器 实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针) 手夯一个STL allocator和对象内存分析组件 std::condition_variable 的信号丢失问题 踩坑一处(GCC)STL std::async 实现BUG导致的crash问题 GCC 14的一个warning to error BUG 给xresloader(Excel导表工具)增强UE读表支持(包含蓝图,Blueprint) Opentelemetry社区在gRPC的几个链接问题(静态库和动态库混用,musl工具链,符号裁剪) Excel转表工具(xresloader)的新验证器(验证外部Excel和文本数据,唯一性和自定义规则) protobuf v22和gRPC v1.55版本升级的依赖变化和upb适配 关于protobuf近期版本(v20/v3.20+)和 gRPC v1.54版本在某些编译环境下的一些链接和编译问题 xresloader-Excel导表工具链的近期变更汇总 打通游戏服务端框架的C++20协程改造的最后一环 Opentelemetry-cpp的Logs模块标准更新(涉及近期版本:1.8-1.9的BREAK CHANGES) 给cmake-toolset和工具链(curl等)加HTTP/2和HTTP/3支持 又开新坑之 coredns 插件: nftables和filter 关于opentelemetry-cpp社区对于C++ Head Only组件单例和符号可见性的讨论小记 填个转表工具 xresloader 去年的坑(数组尾部裁剪) 集成 upb 和 lua binding 的踩坑小记 libcopp对C++20协程的接入和接口设计 再度优化GCC、LLVM、Clang、libc++、libc++abi等套件的构建脚本 游戏服务的分布式事务优化(二)- 事务管理 游戏服务的分布式事务优化(一)- Write Ahead Log(WAL) 模块 记录一些bazel适配用编译选项 测试现代化硬件C++浮点数性能和一致性 适配Boringssl和OpenSSL 3.0 近期cmake-toolset的一些适配问题 再次重构LLVM+Clang+libcxx+libc++abi+其他相关工具的构建流程 重构基于CMake的构建工具链 新版GCC和LLVM+Clang终于Release啦 折腾一下nftables下的双拨 [C++20] Module partitions和符号交叉引用(声明和实现分离) [Rust] 实现一个线程安全且迭代器可以保存的链表 基于protobuf的代码生成 几个使用protobuf中C++接口的Arena的坑 Amazon Aurora DB存储引擎论文阅读小记 近期对libatapp的一些优化调整(增加服务发现和连接管理,支持yaml等) xresloader转表工具链增加了一些新功能(map,oneof支持,输出矩阵,基于模板引擎的加载代码生成等) 在游戏服务器中使用分布式事务 libcopp接入C++20 Coroutine和一些过渡期的设计 libatbus 的大幅优化 nftables初体验 容器配置开发环境小计 PALM Tree - 适合多核并发架构的B+树 - 论文阅读小记 跨平台协程库 - libcopp 简介 C++20 Coroutine 性能测试 (附带和libcopp/libco/libgo/goroutine/linux ucontext对比) 尝鲜Github Action 一些xresloader(转表工具)的改进 protobuf、flatbuffer、msgpack 针对小数据包的简单对比 协程框架(libcopp) 小幅优化 Excel转表工具(xresloader) 增加protobuf插件功能和集成 UnrealEngine 支持 Anna(支持任意扩展和超高性能的KV数据库系统)阅读笔记 C++20 Coroutine libcopp merge boost.context 1.69.0 Google去中心化分布式系统论文三件套(Percolator、Spanner、F1)读后感 Rust玩具-企业微信机器人通用服务 使用ELK辅助监控开发测试环境服务质量和问题定位 2018年的新通用伪随机数算法(xoshiro / xoroshiro)的C++(head only)实现 Webpack+vue+boostrap+ejs构建Web版GM工具 Rust的第二次接触-写个小服务器程序 理解和适配AEAD加密套件 atsf4g-co的进化:协程框架v2、对象路由系统和一些其他细节优化 协程框架(libcopp)v2优化、自适应栈池和同类库的Benchmark对比 可执行文件压缩 初识Rust 使用restructedtext编写xresloader文档 atframework的etcd模块化重构 C++的backtrace ECDH椭圆双曲线(比DH快10倍的密钥交换)算法简介和封装 protobuf-net的动态Message实现 pbc的proto3接入 atgateway内置协议流程优化-加密、算法协商和ECDH 整理一波软件源镜像同步工具+DevOps工具 Blog切换到Hugo libcopp v2的第一波优化完成 libcopp(v2) vs goroutine性能测试 libcopp的线程安全、栈池和merge boost.context 1.64.0 GCC 7和LLVM+Clang+libc++abi 4.0的构建脚本 libatbus的几个藏得很深的bug 用cmake交叉编译到iOS和Android 开源项目得一些小维护 atapp的c binding和c#适配 对象路由系统设计 2016年总结 近期的一个协程流程BUG 重写了llvm+clang+libc++和libc++abi的构建脚本 atsf4g完整游戏工程示例|I'm OWenT atframework基本框架已经完成|I'm OWenT
C++20 Text Formatting/fmtlib 适配问题小记
owent · 2021-09-05 · via I'm OWenT

blog-website

前言

C++20 正式发布已经有一段时间了。其中 Text Formatting 是一个我个人比较感兴趣的新组件。它主要是解决了之前字符串格式化库 ( printf 系 ) 的效率问题和运行时安全的问题。 并且新的格式设置的形式也比较友好。相关规范和用法可以参见:

  • http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0645r10.html#format.formatter
  • http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2216r3.html
  • https://en.cppreference.com/w/cpp/utility/format

在C++ 20能够普及以前,我们可以用 fmtlib 这个库来完成一样的功能。实际上我们项目组已经在这么做了。在使用 fmtlib 之前,比如我们按玩家打日志大概是这种形式。

WLOGDEBUG("player %s(%u:%llu) int %d string %s string_view %s from server %llx", 
          user.get_open_id().c_str(), user.get_zone_id(),
          static_cast<unsigned long long>(user.get_user_id()),
          123, str.c_str(), str_v.data(),
          static_cast<unsigned long long>(server_inst_id));

之所以使用 static_cast<unsigned long long>(...) 是因为这是在跨平台跨编译器开启 -Wall/W4 的情况下,对 uint64_t 执行printf系接口报warning最简单的方法。

在使用 fmtlib 之后,我们按玩家打印日志可以变成:

FWLOGDEBUG("int {} string {} string_view {} from server {:#x}", 
          user, 123, str.c_str(), str_v.data(), server_inst_id);

是不是简洁多了?

于此同时,我们的构建系统改成了会检测编译环境是否支持 C++20 Text Formatting ,在支持的情况下使用 C++20 Text Formatting ,在不支持的情况下使用 fmtlib 。 但是因为目前各大编译器和STL实现中,C++20 Text Formatting 还处于experiment阶段。并且 C++20 Text Formattingfmtlib 多多少少还是有一些不同的地方。 我们跨平台上就踩了一些坑,特此记录一下以便互相交流。

枚举类型的差异

首先是对枚举类型处理的差异。如果没有自定义 formatter ,在 fmtlib 里是能够自动转换成整数类型的输出的,但是(至少是 MSVC)的 C++20 Text Formatting 实现里是不会自动转换的,我翻了一下ISO文档好像是没看到对是否隐式转换的说明。这里会造成一处适配上的问题。比如一些小伙伴习惯用的编译器不支持 C++20 Text Formatting 而fallback到了使用 fmtlib 实现的时候,可能会忘记这个手动转换。那么切到某些编译环境上使用 C++20 Text Formatting 的时候可能会编译不过,需要再适配一次。

Visual Studio 2019 version 16.10(MSVC 1929)的BUG

Visual Studio 2019 version 16.10(MSVC 1929)的第一个版本的实现中 format_to_n 实现有个BUG。里面某一层调用本该用它内部的 _Count()_Size() 接口。但是用了 size() 。会导致编译不过。

当时版本的代码已经找不到了,并且最新版本已经修复了这个问题。碰到同类问题的童鞋们可能直接更新VS版本就行了。

Visual Studio 2019 version 16.11 的变化

我们项目组的构建系统使用的是cmake,在cmake 3.20以前,是不支持检测设置 CXX_STANDARD23 的。在我们早期用的cmake版本中,最大支持的是C++20的检测,所以最大版本号写的就是 set(CXX_STANDARD 20)

这种情况下,同时在 Visual Studio 2019 version 16.10 之前,VS还没有 /std:c++20 选项,所以cmake会把C++标准设为 /std:c++latest 。但是 Visual Studio 2019 version 16.11 开始有了一些变化。相关的信息如下:

  • https://github.com/microsoft/STL/issues/1814
  • https://github.com/microsoft/STL/issues/1814#issuecomment-845572895

简单地说就是 MSVC 对一些C++20特性的实现还没有进入ABI稳定期,所以在使用 /std:c++20 时并不会启用 C++20 里的一些内容,包括但不限于Text Formatting,Ranges等等。 所以我们现在一方面是对高版本的cmake开到了 set(CXX_STANDARD 23) ,这样在 MSVC 下又会使用到 /std:c++latest 。另一方面针对MSVC的这种情况,在构建系统中对 C++20 Text Formatting 的检测脚本做了适配。

fmtlib 8.0

fmtlib 8.0 的接口变化

fmtlib 其实有一个比直接用 C++20 Text Formatting 更好的地方。就在在GCC和Clang下,fmtlib 可以编译期验证输入格式字符串的合法性。 但是这个特性在 fmtlib 8.0 开始有一个很大的变化。

首先,是各类format接口变成了这种形式。

template <typename... T>
FMT_INLINE auto format(format_string<T...> fmt, T&&... args) -> std::string {
  return vformat(fmt, fmt::make_format_args(args...));
}

可以看到格式字符串现在是一个 format_string 对象。那么这个 format_string 对象是啥样呢?

#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
// Workaround broken conversion on older gcc.
template <typename... Args> using format_string = string_view;
template <typename S> auto runtime(const S& s) -> basic_string_view<char_t<S>> {
  return s;
}
#else
template <typename... Args>
using format_string = basic_format_string<char, type_identity_t<Args>...>;
// Creates a runtime format string.
template <typename S> auto runtime(const S& s) -> basic_runtime<char_t<S>> {
  return ;
}
#endif

可以看到,在GCC 4.9以上或其他编译器,format_string 是其内部实现的 basic_format_string 这个结构。那么我们再来看看 basic_format_string

template <typename Char, typename... Args> class basic_format_string {
 private:
  basic_string_view<Char> str_;

 public:
  template <typename S,
            FMT_ENABLE_IF(
                std::is_convertible<const S&, basic_string_view<Char>>::value)>
  FMT_CONSTEVAL basic_format_string(const S& s) : str_(s) {
    static_assert(
        detail::count<
            (std::is_base_of<detail::view, remove_reference_t<Args>>::value &&
             std::is_reference<Args>::value)...>() == 0,
        "passing views as lvalues is disallowed");
#ifdef FMT_HAS_CONSTEVAL
    if constexpr (detail::count_named_args<Args...>() == 0) {
      using checker = detail::format_string_checker<Char, detail::error_handler,
                                                    remove_cvref_t<Args>...>;
      detail::parse_format_string<true>(str_, checker(s, {}));
    }
#else
    detail::check_format_string<Args...>(s);
#endif
  }
  basic_format_string(basic_runtime<Char> r) : str_(r.str) {}

  FMT_INLINE operator basic_string_view<Char>() const { return str_; }
};

这里的重点就来了。我们能看到, basic_format_string 的构造函数加上了 FMT_CONSTEVAL ,其实就是 consteval 关键字。它的含义是可在编译期求值(注意和 const 关键字区分开来,一个函数和类型申明可以是 consteval 但不是 const 的)。再加上 format 接口的申明是 format_string ( 我们关注它是 basic_format_string 的情况。),所以它接受一次隐式类型转换。即,调用 format 的时候格式字符串参数只要能隐式类型转换到 basic_format_string (注意要求编译期转换)就可以。这样我们就可以使用类似 fmt::format("Hello {}", "world") 这种写法。因为 "Hello {}" 是编译期常量。

但是在实际使用中,我们时而会对 format 接口包装一层,增加一下自己的处理。比如我们的日志接口:

template <class FMT, class... TARGS>
void format_log(const caller_info_t &caller, FMT &&fmt_text, TARGS &&...args) {
  // 我们会在这里处理额外的调用者信息处理和使用自己的缓冲区
  // 然后调用 std::format_to_n(...) 或 fmt::format_to_n(...)
}

那么在这种情况下,格式字符串的类型是通用引用(Universal Reference) FMT&& 。在使用 format_log(caller, "Hello {}", "world") 时。FMT 会被推断为 const char[9]& 。然后传给 fmt::format_to_n(...) 。这时候对 fmt::format_to_n(...) 的调用其实就不再是编译期可以求值的 constexpr 了(因为上层的函数签名没有这个保证)。如果直接把 std::forward<FMT>(fmt_text) 转发给 fmt::format_to_n(...) ,就会报出类似下面这种错误。

/usr/include/fmt/core.h:2835:17: note: ‘consteval fmt::v8::basic_format_string<Char, Args>::basic_format_string(const S&) [with S = fmt::v8::basic_format_string<char, test_custom_object_for_log_formatter&>; typename std::enable_if<std::is_convertible<const S&, fmt::v8::basic_string_view<Char> >::value, int>::type <anonymous> = 0; Char = char; Args = {test_custom_object_for_log_formatter}]’ is not usable as a ‘constexpr’ function because:
 2835 |   FMT_CONSTEVAL basic_format_string(const S& s) : str_(s) {
      |                 ^~~~~~~~~~~~~~~~~~~
/usr/include/fmt/core.h:2835:51: error: call to non-‘constexpr’ function ‘fmt::v8::basic_format_string<Char, Args>::operator fmt::v8::basic_string_view<Char>() const [with Char = char; Args = {test_custom_object_for_log_formatter&}]’
 2835 |   FMT_CONSTEVAL basic_format_string(const S& s) : str_(s) {
      |                                                   ^~~~~~~
/usr/include/fmt/core.h:2853:14: note: ‘fmt::v8::basic_format_string<Char, Args>::operator fmt::v8::basic_string_view<Char>() const [with Char = char; Args = {test_custom_object_for_log_formatter&}]’ declared here
 2853 |   FMT_INLINE operator basic_string_view<Char>() const { return str_; }
      |              ^~~~~~~~

我们也不能用类似 basic_format_string 的实现方式包一层数据结构然后申明构造函数为 constexpr 。因为C++规定隐式类型转换只能执行一层,如果我们包一层,就会影响API的易用度。

解决方法其实也比较简单,我们得关注 fmtlib 的内部实现和类型,参数直接传入 basic_format_string 就行了,比如函数签名改成这样:

template <class... TARGS>
void format_log(const caller_info_t &caller, fmt::string_view<TARGS...> fmt_text, TARGS &&...args) {
  // 我们会在这里处理额外的调用者信息处理和使用自己的缓冲区
  // 然后调用 std::format_to_n(...) 或 fmt::format_to_n(...)
}

fmtlib 8.0.1 的一处BUG

fmtlib 8.0.0-8.0.1 版本有一个小BUG,也和我们上面的调用形式相关。其实按我们上面的改法,使用的时候也是编译不过的。 但是同样的形式,我们使用 fmt::format(...)fmt::format_to(...) 是没问题的,仅仅是 fmt::format_to_n(...) 会报错,为什么呢?我们可以先来回顾一下 fmt::format(...) 的声明:

template <typename... T>
FMT_INLINE auto format(format_string<T...> fmt, T&&... args) -> std::string {
  return vformat(fmt, fmt::make_format_args(args...));
}

再来看看 fmt::format_to_n(...) 的申明:

template <typename OutputIt, typename... T,
          FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
FMT_INLINE auto format_to_n(OutputIt out, size_t n, format_string<T...> fmt,
                            const T&... args) -> format_to_n_result<OutputIt> {
  return vformat_to_n(out, n, fmt, fmt::make_format_args(args...));
}

看出来没?关键就在于 fmt::format(...) 里后面的参数是 通用引用(Universal Reference)fmt::format_to_n(...)常量引用 。 并且在这个版本里 fmt::make_format_args(...) 的接口声明如下:

template <typename Context = format_context, typename... Args>
constexpr auto make_format_args(const Args&... args)
    -> format_arg_store<Context, Args...> {
  return {args...};
}

这样,在 fmt::format(...)fmt::format_to(...) 的实现中,最终传入的 format_arg_store<Context, T...> 类型和解析参数的时候的 format_string<T...>T... 类型是不一样的。 然后就会通过 SFINAE 机制去尝试所有可能的类型转换,最后失败出现编译错误。

这个问题我已经提了 IssuePR 了。目前已经合入了,估计下个版本就会包含进去。

在当前 fmtlib 的主干版本里,fmt::make_format_args(...) 已经改成了:

template <typename Context = format_context, typename... Args>
constexpr auto make_format_args(Args&&... args)
    -> format_arg_store<Context, remove_cvref_t<Args>...> {
  return {std::forward<Args>(args)...};
}

其实也从侧面解决了这个问题。

写在最后

无论是 C++20 Text Formatting ,还是 fmtlib 8.0都属于比较新兴的事物。各类实现也都还不是非常完善,多多少少会碰到一些问题。欢迎有兴趣同踩过坑的小伙伴们提出意见互相交流。