存储std::format_args时的生命周期问题及替代方案咨询
请查看以下代码:
#include <print> #include <exception> #include <string> #include <format> class exception_t : public std::exception { public: template<class... args_t> exception_t(std::string_view users_fmt, args_t&&... args) : m_users_fmt(users_fmt) , m_format_args(std::move(std::make_format_args(args...))) {} char const* what() const noexcept override { thread_local static std::string s = std::vformat(m_users_fmt, m_format_args); return s.c_str(); } private: std::string m_users_fmt; std::format_args m_format_args; }; int main() try { throw exception_t("{}", 42); } catch (std::exception& e) { std::println("{}", e.what()); }
在MSVC 2022 v143搭配SDK 10.0.19041.0环境下调试这段代码时,构造exception_t期间m_format_args拥有有效值,但调用what()时m_format_args的值已失效,程序通常无法输出42,请问哪里出错了?
更新需求
以上是最小化代码片段,初衷是将格式化操作与异常/错误/警告报告完全分离。希望将参数以标准类型存储(例如存入磁盘),之后由另一个进程读取并使用本地化语言的格式字符串展示数据,因此不想在构造函数中调用std::vformat。是否存在std::format_args的替代方案?例如,能否直接存储参数?
解答
原问题错误原因
你的问题出在std::format_args的生命周期依赖上。std::format_args是一个轻量级的类型擦除视图,它并不存储传入的参数本身,仅持有对这些参数的引用。
在exception_t的构造函数中,std::make_format_args(args...)生成的format_args对象,完全依赖于构造函数参数中转发的args...的生命周期。当构造函数执行完毕后,这些args(比如你传入的字面量42这类临时对象)会被销毁,导致m_format_args引用的内存变为无效的垃圾数据。后续调用what()时使用这个失效的format_args属于未定义行为,因此无法正确输出结果。
适配需求的替代方案
如果要实现格式化与异常报告分离,且需要持久化参数供跨进程使用,std::format_args因仅为视图类型完全不适用,以下是几种可行方案:
使用std::dynamic_format_arg_store(C++20+)
C++20提供的std::dynamic_format_arg_store可以存储格式化参数的副本,而非引用,其生命周期与对象本身一致。修改后的示例代码如下:#include <print> #include <exception> #include <string> #include <format> class exception_t : public std::exception { public: template<class... args_t> exception_t(std::string_view users_fmt, args_t&&... args) : m_users_fmt(users_fmt) , m_args(std::make_format_args_store<std::format_context>(std::forward<args_t>(args)...)) {} char const* what() const noexcept override { thread_local static std::string s; try { s = std::vformat(m_users_fmt, std::make_format_args(m_args)); } catch(...) { s = "Format error"; } return s.c_str(); } private: std::string m_users_fmt; std::dynamic_format_arg_store<std::format_context> m_args; }; int main() try { throw exception_t("{}", 42); } catch (std::exception& e) { std::println("{}", e.what()); }该方案完全适配
std::format生态,但std::dynamic_format_arg_store无法直接序列化,若需持久化到磁盘,需额外实现序列化逻辑。存储可序列化的参数容器
将参数存储为std::vector<std::any>或自定义的std::variant(包含所有可能的参数类型),同时记录参数类型信息。需要格式化时,将std::any转换回原类型再传递给std::format。此方式需自行处理类型转换与序列化,但优势是可轻松持久化,供其他进程读取。预序列化为通用格式
将参数直接序列化为JSON、XML等通用格式存储,其他进程读取后可自行解析并格式化。该方案最适合跨进程、跨语言场景,但需引入序列化库(如nlohmann/json)。
内容的提问来源于stack exchange,提问作者Jeroen Lammertink

