C++函数中使用两个参数包是否属于未定义行为?
我实现了一个包含两个参数包的函数,能够控制参数分别归入两个包中,代码如下:
#include <iostream> using namespace std; template< typename... Args0, typename... Args1> void func( Args0&&... args0, Args1&&... args1 ) { cout << "args0: "; (cout << ... << args0); cout << '\n'; cout << "args1: "; (cout << ... << args1); cout << '\n'; } int main() { func<int, int, int>( 1, 2, 3, 4, 5, 6 ); cout << '\n'; func<int, int>( 1, 2, 3, 4, 5, 6 ); } //Output from both GCC and Visual Studio. // args0: 123 // args1: 456 // // args0: 12 // args1: 3456
该代码在GCC和Visual Studio中均可正常运行,但我原以为这是无效代码。请问这属于未定义行为吗?我能否安全地使用该写法?
补充使用示例:
template<typename... Args> ostream & operator<<( ostream & out, const tuple<Args...> & tup ) { auto func = [&]( const auto&... args_first, const auto&... arg_last ) { out << "{ "; ((out << args_first << " , "), ...); ((out << arg_last), ...); out << " }"; }; apply( func, take_firsts( tup ), take_last( tup ) ); return out; }
回答
这种写法不属于未定义行为,在特定场景下可以安全使用,但需要明确其适用边界:
显式指定模板参数时的合法性
C++标准并未禁止函数模板拥有多个参数包,只是规定:如果依赖编译器自动推导模板参数,当参数列表中存在多个非末尾的参数包时,编译器无法确定参数包的划分边界(会直接触发编译错误)。但如果像你这样显式指定模板参数,编译器可以明确知道Args0的类型数量,从而将对应数量的实参归入args0,剩余实参归入args1——这种处理是符合标准规则的,GCC、VS及Clang等主流编译器都支持该场景。依赖自动推导的风险
如果你省略模板参数列表直接调用func(1,2,3,4),编译器会报错,因为无法判断哪些实参属于Args0、哪些属于Args1。这是标准规定的编译错误,而非未定义行为。补充示例中的泛型lambda场景
你的补充示例中,泛型lambda使用双参数包的写法,依赖apply将拆分后的tuple元素分别传入两个参数包。这种场景下,take_firsts和take_last已经明确了两个参数包的元素数量,apply会将每个tuple的元素对应到lambda的参数包中,属于合法用法(需C++20及以上支持泛型lambda的参数包语法)。可移植性说明
目前主流编译器(GCC、Clang、MSVC)都支持显式指定模板参数的双参数包写法,但如果需要兼容更老旧的编译器版本,建议先做兼容性测试。
总结:只要你通过显式指定模板参数或明确参数包划分边界的方式(如补充示例中的tuple拆分)使用双参数包,这种写法是安全合规的,不属于未定义行为。
内容的提问来源于stack exchange,提问作者Generic Name

