我们经常需要确定某个类型是否为某个模板的实例,写一个 trait 即可,这里我们使用了一个非常简单的偏特化:
template <typename T>
struct A {};
template <typename>
struct is_instantiation_of_A : std::false_type {};
template <typename T>
struct is_instantiation_of_A<A<T>> : std::true_type {};
然而,有时我们希望 在偏特化中 增加额外的约束,例如要求 A 的模板参数 T 须为 trivial 类型,以下 2 种写法都是错误的:
template <typename>
struct is_trivial_instantiation_of_A : std::false_type {}; // 基模板
template <typename T, std::enable_if_t<std::is_trivial_v<T>>>
struct is_trivial_instantiation_of_A<A<T>> : std::true_type {}; // 错误
template <typename T, typename = std::enable_if_t<std::is_trivial_v<T>>>
struct is_trivial_instantiation_of_A<A<T>> : std::true_type {}; // 错误
注意: 如果不是偏特化上下文,这两种写法都对!
上述第 1 种错误写法的问题很有意思:在这个偏特化中,第 2 个模板参数依赖于第 1 个模板参数 T,而由于 T 处于一个待匹配的偏特化之中,T 依赖于“到底匹配到了哪个偏特化”这个判断,而要完成这个判断,第 2 个模板参数又必须是确定的 —— 此时就形成了模板参数 1 和 2 的相互依赖,类型推断无法完成,这是一种特殊的 非推断上下文。
第 2 种错误写法的问题很明显,偏特化不能具有默认模板参数,因此我们常用的“增加占位模板参数作为开关”的技巧失效了;此外也有与第 1 种一样的错误,这里的模板参数 2 也与模板参数 1 相互依赖。
正确的写法是:
template <typename, typename = void>
struct is_trivial_instantiation_of_A : std::false_type {}; // 基模板
template <typename T>
struct is_trivial_instantiation_of_A<
A<T>,
std::enable_if_t<std::is_trivial_v<T>> > : std::true_type {}; // 正确
即 在基模板中增加一个有默认值的待定模板参数,它如同一个“插槽”,根据需要可以在特化中引入任何额外的 trait;阅读标准库,可发现这种用法比较常见。
测试(通过编译即可):
int main() {
static_assert( is_instantiation_of_A<A<int>>::value, "");
static_assert( is_trivial_instantiation_of_A<A<int>>::value, "" );
static_assert( !is_trivial_instantiation_of_A<A<std::string>>::value, "" );
return 0;
}