参数包展开用作C++概念非包参数的代码是否符合标准?
核心问题验证
先看这段代码:
template<typename T> concept A = true; template<typename... T> concept B = A<T...>;
Clang与GCC会在第二行报错:pack expansion used as argument for non-pack parameter of concept,而MSVC可以正常编译。
结论:这段代码不符合C++标准,Clang和GCC的行为是正确的,MSVC的编译支持属于非标准扩展。
根据C++标准,概念的模板参数列表如果不是参数包形式,实例化时必须传入数量完全匹配的模板实参。concept A只接受单个模板参数,而A<T...>试图将参数包展开后传入——无论参数包大小是否为1,标准都不允许对非参数包的概念模板进行这种参数包展开操作,这属于格式错误。
具体场景分析
我们需要定义一个带参数包的概念single_integral_or_all_floating_point,要求:当参数包是单个整数类型,或所有类型都是浮点类型时,概念值为true,否则为false。
旧类型特性实现
使用std::is_integral和折叠表达式的写法如下:
template<typename... T> concept single_integral_or_all_floating_point = std::is_integral<T...>::value || (... && std::is_floating_point<T>::value);
当sizeof...(T)不为1时,std::is_integral<T...>会触发替换失败,第一个约束自动不满足,最终由第二个折叠表达式判断是否全为浮点类型。
尝试用C++概念直接替换
如果直接改用标准库概念std::integral替换std::is_integral,会写出这样的代码:
template<typename... T> concept single_integral_or_all_floating_point = std::integral<T...> || // error: pack expansion used as argument for non-pack parameter of concept (... && std::floating_point<T>);
此时Clang和GCC会抛出之前提到的错误,而MSVC虽然能编译,但这是不符合标准的行为。
兼容标准的 workaround
要在符合C++标准的前提下实现需求,需要添加一个辅助模板来限制参数包只能为单个类型时才暴露该类型,否则触发替换失败:
template<typename... T> struct only {}; template<typename T> struct only<T> { using type = T; }; template<typename... T> concept single_integral_or_all_floating_point = std::integral<typename only<T...>::type> || (... && std::floating_point<T>);
当参数包大小不为1时,only<T...>::type不存在,std::integral<...>会触发替换失败,第一个约束不生效,转而判断第二个折叠表达式;当参数包大小为1时,only<T>::type就是该类型,此时会判断该类型是否为整数类型。
标准合规带来的繁琐性
从这个场景可以看到,在需要判断参数包是否为单个满足某概念的类型时,直接使用概念会比旧类型特性更繁琐。这是因为标准对概念实参的参数包展开做了严格限制以避免歧义,但也确实增加了这类场景的代码复杂度。
内容的提问来源于stack exchange,提问作者wanghan02

