为何C++模板特化的默认参数必须为void才能生效?
enable_if_t的第二个参数必须设为void才能生效? 咱们先回到你的代码场景:你定义了一个带默认模板参数的主模板,想用偏特化根据类型条件生成不同的函数指针数组,但发现把enable_if_t的第二个参数设为T时特化完全没效果,换成默认的void就正常工作。这背后其实是C++模板偏特化的参数匹配规则在起作用。
先理清主模板和特化的参数对应关系
先看你的主模板定义:
template <typename T, typename = void> constexpr details::subscript_function<T> my_temp[] = { &details::X<T>, &details::Y<T> };
这里第二个模板参数是一个类型参数,默认值是void。也就是说,当你写my_temp<decltype(foo)>时,编译器会自动把第二个参数补成void,相当于实例化my_temp<vec, void>。
再看你出问题的特化版本:
template <typename T> constexpr details::subscript_function<T> my_temp<T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>>[] = { &details::X<T>, &details::Y<T>, &details::Z<T> };
这里enable_if_t<B, T>等价于typename enable_if<B, T>::type——当条件B为真时,它的结果就是T类型。
为什么设为T不生效?
当你把enable_if_t的第二个参数设为T时,这个特化的第二个模板参数就变成了T(也就是vec类型)。但主模板实例化时用的第二个参数是void,两者类型完全不一样,编译器会认为这个特化和当前的实例化请求不匹配,直接跳过它,继续用主模板的定义。
简单说:特化的参数列表必须和主模板实际被实例化时的参数完全对应,否则特化不会被选中。
为什么设为void就正常?
如果把特化改成这样(省略enable_if_t的第二个参数,默认就是void):
template <typename T> constexpr details::subscript_function<T> my_temp<T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>>>[] = { ... };
此时特化的第二个参数就是void,和主模板实例化时的第二个参数完全一致。只要is_floating_point_v<...>的条件为真,SFINAE就会通过,这个特化就会被优先选中——因为它是主模板的一个更具体的版本(主模板接受任意第二个类型参数,而这个特化只接受void且满足条件的情况)。
额外补充:SFINAE的角色
这里SFINAE的作用是:当条件不满足时,enable_if_t会触发替换失败,这个特化会被编译器丢弃,不会报错。但如果条件满足,特化的参数必须和主模板的实例化参数匹配,才能被选用。你之前设为T的情况,即使条件满足,参数不匹配,特化也不会生效。
要是非得用T当第二个参数怎么办?
如果你想让特化的第二个参数是T,那得修改主模板的默认参数,让它和特化的参数对齐:
// 把主模板的第二个参数默认值改成T template <typename T, typename = T> constexpr details::subscript_function<T> my_temp[] = { &details::X<T>, &details::Y<T> }; // 特化的第二个参数就是T,和主模板默认值匹配 template <typename T> constexpr details::subscript_function<T> my_temp<T, enable_if_t<is_floating_point_v<decltype(details::X(T()))>, T>>[] = { ... };
这样当你实例化my_temp<vec>时,主模板的第二个参数默认是vec,特化的第二个参数也是vec(条件满足时),就能匹配上了。但这种写法不如用void简洁,因为void是一个通用的占位符,不会和任何用户自定义类型冲突,是模板特化里常用的技巧。
总结一下:模板偏特化的参数必须和主模板实例化时的实际参数匹配,主模板第二个参数默认是void,所以特化的第二个参数也得是void才能被选中。设成T的话,参数类型不匹配,特化自然就不生效了。
内容的提问来源于stack exchange,提问作者Jonathan Mee

