折腾:std::format placeholder

编译期:std::format placeholder

背景与目标

现代 C++ 提供了 std::format,比传统的 std::stringstream 更安全、高效,也避免了手动拼接。但对于旧代码或者希望“自动推导格式串”的开发者来说,迁移成本高。

我有一个常用的辅助函数 inner_string(...),基于 stringstream 构建字符串拼接,现在我希望用 std::format 替代它,并自动推导 {} 占位符的数量与格式。

目标是:

  • 保留调用方式不变(如 inner_string(1.23, "abc", 42)
  • 自动生成合适的 format_string 结构
  • 对于 float/double 自动加 {:.2f} 之类的精度
  • 支持 tuple 等复合类型拆解
  • 最好在编译期完成这些逻辑

旧代码方式回顾:stream 拼接

我们之前常用 stringstream 实现类似 print 效果:

1
2
std::stringstream ss;
ss << 123 << "abc" << 1.9 << '\n';

封装过后是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename _Func, typename..._Args>
static void _inner_unpak(_Func fun, const _Args&... args) {
(void)std::initializer_list<int>{
[&](const auto& arg) {
fun(arg);
return 0;
}(args)...
};
}

template<typename..._Args>
inline static void _inner_format(std::ostream& stream, const _Args&... args) {
_inner_unpak([&](const auto& arg) {
stream << arg;
}, args...);
}

template<typename... _Args>
inline static std::string inner_string(const _Args&... args) {
std::stringstream ss;
_inner_format(ss, args...);
return ss.str();
}

这其实已经是很泛化的流拼接工具了,和 std::format 的格式感知能力相比较,缺点是:

  • 不支持精度控制
  • 输出不统一
  • 类型错误编译期无法发现

起点尝试:运行时构造 format string

为了接近 std::format 的风格,第一步尝试是:根据参数个数生成一串 {} 占位符。

1
2
3
4
5
6
7
8
9
10
template<typename... Args>
std::string format_string(const Args&... args) {
std::size_t arg_count = sizeof...(args);
std::string placeholders;
for (std::size_t i = 0; i < arg_count; ++i) {
placeholders += "{}";
if (i < arg_count - 1) placeholders += ' ';
}
return std::vformat(placeholders, std::make_format_args(args...));
}

但是存在几个问题:

  • 每次都运行时构造格式字符串,效率低
  • 无法针对类型生成特定格式(如 float 想输出 {:.2f}
  • 不支持结构体 / tuple 等更复杂类型

编译期自动推导格式字符串:终极方案

我设计了一个完全在编译期推导格式串的系统,基于:

  • consteval 编译期函数
  • concepts 判断是否是 tuple
  • 模板参数展开
  • std::array<char> 拼接字符串
  • C++20 string_view 缓存格式串

类型到格式映射机制

1
2
3
4
5
template <typename T>
struct type_format { static constexpr const char* value = "{}"; };

template <> struct type_format<float> { static constexpr const char* value = "{:.2f}"; };
template <> struct type_format<double> { static constexpr const char* value = "{:.2f}"; };

判断是否 tuple 类型

1
2
template <typename T>
concept is_tuple_like = requires { typename std::tuple_size<T>::type; };

递归收集格式

1
2
3
4
5
6
7
8
9
10
template <typename T, typename Collector>
consteval void collect_type_formats(Collector collector) {
if constexpr (is_tuple_like<T>) {
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
(collect_type_formats<std::tuple_element_t<Is, T>>(collector), ...);
}(std::make_index_sequence<std::tuple_size_v<T>>{});
} else {
collector(type_format<std::decay_t<T>>::value);
}
}

拼接格式串(含分隔符)

1
2
3
4
5
6
7
8
9
10
11
12
13
template <std::size_t Count, char Sep = ' '>
consteval auto make_format_string(const std::array<const char*, Count>& formats, std::size_t used) {
std::array<char, AUTO_FORMAT_LOGGER_MAX_ARGS * 16> buf{};
std::size_t pos = 0;
for (std::size_t i = 0; i < used; ++i) {
const char* f = formats[i];
while (*f) buf[pos++] = *f++;
if (i < used - 1) buf[pos++] = Sep;
}
std::array<char, AUTO_FORMAT_LOGGER_MAX_ARGS * 16> result{};
std::copy_n(buf.begin(), pos, result.begin());
return std::pair{result, pos};
}

外部统一封装接口

1
2
3
4
5
template <typename... Args>
struct type_format_string_placeholders {
static constexpr auto fmt = make_type_format_string_ct<Args...>();
static constexpr auto sv = std::string_view{fmt.first.data(), fmt.second};
};

使用示例

1
2
3
4
5
6
7
constexpr auto fmt = auto_format_rules::detail::make_type_format_string_ct<float, int, float>();
std::string_view sv(fmt.first.data(), fmt.second);

float a = 1.2345f, c = 3.14f;
int b = 42;
std::string result = std::vformat(sv, std::make_format_args(a, b, c));
// 输出: 1.23 42 3.14

也支持 tuple 嵌套:

1
2
using tup = std::tuple<int, float, std::tuple<double>>;
constexpr auto sv = auto_format_rules::detail::type_format_string_placeholders<tup>::sv;

编译期优势对比

方法 编译期 类型感知 精度控制 性能开销
手动写格式串 最低
运行时拼接 {}
本文方案:类型推导 + 缓存 最优

后续扩展想法

  • {name} 风格格式串支持(需配合索引与映射)
  • 自动为 struct 提供格式(结合 C++23 reflexpr
  • 支持单位后缀拼接(如 KBms
  • 编译期格式串验证与调试输出

总结

这个系统完全基于编译期的 constexpr + 模板机制实现,支持自动生成符合类型的格式字符串,并为日志、调试、格式输出等提供了更高的性能与更好的类型安全。

欢迎留言交流优化建议或 fork 后自定义扩展。

------ 本文结束 ------
------ 版权声明:转载请注明出处 ------