使用std::apply结合sscanf解析字符串到tuple时无法完全更新问题
问题:使用std::apply结合sscanf解析字符串到多元素tuple失败
我需要将结构化数据字符串解析到tuple中,不同类型的输入字符串对应的数据类型和排列方式不同,因此希望通过模板和scanf格式来避免变量管理与重复代码。
以下是我尝试的代码,预期能将tuple的成员展开为参数,并根据sscanf的结果更新这些成员:
#include <tuple> #include <cstdio> #include <cassert> int main() { // pure sscanf const char msg[] = "000000000A200890"; const char fmt[] = "%04X%04X%04X%04X"; uint16_t a, b, c, d; sscanf(msg, fmt, &a, &b, &c, &d); assert(a == 0x0000); assert(b == 0x0000); assert(c == 0x0A20); assert(d == 0x0890); // into a one-tuple seems to work std::tuple<uint16_t> onetuple; const char *shortmsg = "10"; const char *shortfmt = "%02X"; std::apply([&shortmsg, &shortfmt](auto &...args) { sscanf(shortmsg, shortfmt, &args...); }, onetuple); assert(std::get<0>(onetuple) == 0x10); // into a tuple std::tuple<uint16_t, uint16_t, uint16_t, uint16_t> dest; std::apply([&msg, &fmt](auto &...args) { sscanf(msg, fmt, &args...); }, dest); assert(std::get<0>(dest) == 0x0000); assert(std::get<1>(dest) == 0x0000); assert(std::get<2>(dest) == 0x0A20); assert(std::get<3>(dest) == 0x0890); return 0; }
但这段代码无法正常运行:纯sscanf的代码块表现符合预期,单元素tuple使用std::apply结合sscanf也能正常更新,但多元素tuple传入std::apply时,tuple完全未被修改。运行环境为C++17(GCC)。
原因分析
问题出在可变参数展开的求值顺序和sscanf的特性上:
- sscanf是C风格可变参数函数,要求传入的指针参数必须严格匹配格式字符串的顺序。
- 在C++17标准中,参数包
&args...的求值顺序是未指定的,编译器可以自由选择参数的传递顺序。GCC的实现中,多元素参数包展开时,指针的传递顺序可能和格式字符串预期的顺序不匹配,导致sscanf写入了错误的内存位置,看起来像是tuple未被修改。 - 单元素tuple不受影响是因为只有一个参数,不存在顺序问题。
解决方案
方案1:用std::index_sequence保证参数顺序
通过std::index_sequence显式按顺序生成tuple成员的指针,确保传递给sscanf的参数顺序完全匹配格式字符串:
#include <tuple> #include <cstdio> #include <cassert> #include <utility> // 内部实现函数,通过index_sequence指定顺序 template<typename Tuple, size_t... Is> void sscanf_into_tuple_impl(const char* msg, const char* fmt, Tuple& t, std::index_sequence<Is...>) { sscanf(msg, fmt, &std::get<Is>(t)...); } // 对外接口,自动生成index_sequence template<typename Tuple> void sscanf_into_tuple(const char* msg, const char* fmt, Tuple& t) { constexpr size_t tuple_size = std::tuple_size_v<Tuple>; sscanf_into_tuple_impl(msg, fmt, t, std::make_index_sequence<tuple_size>{}); } int main() { // pure sscanf验证 const char msg[] = "000000000A200890"; const char fmt[] = "%04X%04X%04X%04X"; uint16_t a, b, c, d; sscanf(msg, fmt, &a, &b, &c, &d); assert(a == 0x0000); assert(b == 0x0000); assert(c == 0x0A20); assert(d == 0x0890); // 单元素tuple验证 std::tuple<uint16_t> onetuple; const char* shortmsg = "10"; const char* shortfmt = "%02X"; sscanf_into_tuple(shortmsg, shortfmt, onetuple); assert(std::get<0>(onetuple) == 0x10); // 多元素tuple验证 std::tuple<uint16_t, uint16_t, uint16_t, uint16_t> dest; sscanf_into_tuple(msg, fmt, dest); assert(std::get<0>(dest) == 0x0000); assert(std::get<1>(dest) == 0x0000); assert(std::get<2>(dest) == 0x0A20); assert(std::get<3>(dest) == 0x0890); return 0; }
方案2:手动指定参数顺序(仅作临时解决)
如果不想封装模板函数,可以在std::apply的lambda中显式列出每个参数,强制顺序:
std::apply([&msg, &fmt](auto& arg0, auto& arg1, auto& arg2, auto& arg3) { sscanf(msg, fmt, &arg0, &arg1, &arg2, &arg3); }, dest);
但这种方式失去了模板的通用性,不推荐用于通用场景。
原理说明
std::index_sequence和std::make_index_sequence会生成从0到tuple_size-1的编译期整数序列,&std::get<Is>(t)...会严格按0、1、2...的顺序生成tuple成员的指针,这些指针会按顺序传递给sscanf,完全匹配格式字符串的预期顺序,彻底避免了参数包展开的顺序问题。
内容的提问来源于stack exchange,提问作者Remy Porter
相关产品推荐
相关产品推荐

