在C++包展开中使用索引自增操作是否属于未定义行为?
问题结论
你担忧的count++在参数包展开中的用法,在C17及更高版本中符合标准,行为确定;C14及更早版本中属于未定义行为,存在兼容性风险。
原理说明
- C++17之前的标准没有限定同一表达式内多个函数实参的求值顺序,且多个对同一变量的副作用操作之间没有序列点时属于明确的未定义行为。你代码中的
std::make_tuple(ConvertArg<Args>(args, count++)...)在展开后,多个count++的执行顺序完全由编译器决定,可能得到非预期的下标值,甚至出现运行异常。 - C17标准新增了明确规则:花括号初始化列表、模板参数包展开、折叠表达式中的各个元素求值严格遵循从左到右的顺序,前一个元素的所有副作用完成后才会执行下一个元素的求值。因此在C17及以上标准下,
count++会严格按0、1、2...sizeof...(Args)-1的顺序递增,行为完全符合预期。
兼容方案
如果你的项目需要兼容C++14及更早版本,可改用编译期下标序列的写法完全规避求值顺序问题,参考实现如下:
template<class ...Args, std::size_t ...Is> std::function<void(ArgCollection)> GetConvertImpl(std::function<void(Args...)> thing, std::index_sequence<Is...>) { constexpr std::size_t argCount = sizeof...(Args); return [argCount, thing](const ArgCollection& args) -> void { if (args.argCount != argCount) throw std::invalid_argument("Invalid number of arguments"); auto convertedArgs = std::make_tuple(ConvertArg<Args>(args, Is)...); std::apply(thing, convertedArgs); }; } template<class ...Args> std::function<void(ArgCollection)> GetConvert(std::function<void(Args...)> thing) { return GetConvertImpl(std::move(thing), std::index_sequence_for<Args...>{}); }
该写法只要编译器支持C++11及以上标准,行为都是确定的,不存在未定义风险。
另外补充说明:你给出的示例代码存在笔误,函数参数命名为thing,但lambda捕获列表写的是method,实际开发中需要修正该问题。
内容的提问来源于stack exchange,提问作者UKMonkey
相关产品推荐
相关产品推荐

