如何在编译时配置格式化参数并构造constexpr格式化字符串?
看起来你想要完全在编译期把width、precision这类数值参数转换成字符串,再拼接成符合std::format/fmt规范的格式串,还能复用这些格式串对吧?你之前遇到的E0028错误,核心原因是你写的to_String函数返回的string_view指向了consteval函数内部的局部数组——函数执行完后这块局部内存就销毁了,string_view就变成了悬空引用,编译器自然会报错。
那怎么解决这个问题呢?关键是要让编译期生成的字符串存储在静态生命周期的内存里,比如用std::array来持有编译期字符串,再实现编译期的字符串拼接工具。下面给你一个可行的完整方案:
第一步:实现编译期工具函数
先写两个核心工具:编译期数值转字符串,以及编译期多字符串拼接。这些工具都用consteval保证完全在编译期执行:
#include <array> #include <string_view> #include <print> #include <format> #include <type_traits> // 编译期计算C风格字符串长度 consteval std::size_t cstr_len(const char* s) { std::size_t len = 0; while (s[len] != '\0') ++len; return len; } // 编译期无符号整数转字符串,返回带终止符的std::array template<std::unsigned_integral auto U> consteval auto uint_to_cstr() { if constexpr (U == 0) { return std::array<char, 2>{'0', '\0'}; } else { // 编译期计算数字的位数 constexpr auto num_digits = []() { std::size_t cnt = 0; auto temp = U; do { ++cnt; temp /= 10; } while (temp != 0); return cnt; }(); // 用array存储字符串(+1是为了终止符) std::array<char, num_digits + 1> arr{}; auto temp = U; std::size_t pos = num_digits; do { arr[--pos] = '0' + (temp % 10); temp /= 10; } while (temp != 0); arr[num_digits] = '\0'; return arr; } } // 编译期计算多个字符串的总长度 template<typename... Strs> consteval auto total_str_len(const Strs&... strs) { std::size_t len = 0; ((len += [&]() { if constexpr (std::is_same_v<std::decay_t<Strs>, const char*>) { return cstr_len(strs); } else if constexpr (std::is_array_v<std::decay_t<Strs>>) { // 对于std::array<char, N>,有效长度是N-1(去掉终止符) return std::size(strs) - 1; } }()), ...); return len; } // 编译期拼接多个字符串,返回带终止符的std::array template<typename... Strs> consteval auto concat_strs(const Strs&... strs) { constexpr auto total_len = total_str_len(strs...); std::array<char, total_len + 1> result{}; std::size_t pos = 0; (([&]() { if constexpr (std::is_same_v<std::decay_t<Strs>, const char*>) { std::size_t s_len = cstr_len(strs); for (std::size_t i = 0; i < s_len; ++i) { result[pos++] = strs[i]; } } else if constexpr (std::is_array_v<std::decay_t<Strs>>) { std::size_t s_len = std::size(strs) - 1; for (std::size_t i = 0; i < s_len; ++i) { result[pos++] = strs[i]; } } }()), ...); result[pos] = '\0'; return result; }
第二步:定义你的格式化结构体
现在你可以用上面的工具,轻松构造编译期格式串,还能复用已有的格式串:
struct A { static constexpr unsigned width = 10, precision = 4; // 先把数值参数转成编译期字符串 static constexpr auto width_str = uint_to_cstr<width>(); static constexpr auto precision_str = uint_to_cstr<precision>(); // 构造单个数字的格式串:"{:10.4f}" static constexpr auto fmt_Str_Num = concat_strs( "{:", width_str, ".", precision_str, "f}" ); // 构造Vec2的格式串,复用数字格式串:"({:10.4f},{:10.4f})" static constexpr auto fmt_Str_Vec2 = concat_strs( "(", fmt_Str_Num, ",", fmt_Str_Num, ")" ); // 提供std::string_view方便直接传给std::print/format static constexpr std::string_view fmt_View_Num = { fmt_Str_Num.data(), std::size(fmt_Str_Num) - 1 }; static constexpr std::string_view fmt_View_Vec2 = { fmt_Str_Vec2.data(), std::size(fmt_Str_Vec2) - 1 }; };
第三步:使用编译期格式串
在main函数里,你就可以像你想要的那样使用这些格式串了:
int main() { float x = 1.23456789f; float y = 123456789.0f; // 打印单个数字 std::print(A::fmt_View_Num, x); std::println(); // 换行方便查看 // 格式化Vec2并存储为字符串 std::string str = std::format(A::fmt_View_Vec2, x, y); std::print("Vec2: {}", str); return 0; }
关键细节说明
为什么用std::array?
因为consteval函数里的局部std::array如果作为返回值,会被初始化到静态存储区(因为是static constexpr成员),所以它的data()指向的内存是永久有效的,不会出现悬空引用的问题。为什么要提供std::string_view?
std::print和std::format可以直接接受std::string_view,而std::array<char, N>需要转换成string_view来去掉多余的终止符(或者你也可以直接传fmt_Str_Num.data(),但string_view更安全,能明确指定长度)。复用格式串的逻辑
因为fmt_Str_Num是static constexpr的编译期数组,所以在拼接fmt_Str_Vec2时,编译器会直接把它的内容和其他字符串拼接在一起,完全在编译期完成,没有运行时开销。
内容来源于stack exchange

