一天一个C++大佬同事,突然截图过来一段代码:这写的啥呀,啰里吧嗦的,这个构造函数模板参数T1感觉是多余的呀
cpp
template<class T>
class TestClass
{
public:
TestClass(){}
//函数1
template<class T1 = T, std::enable_if_t<std::is_same_v<int, T1>,bool> = true>
TestClass()
{
//balabala
}
//函数2
template<class T1 = T, std::enable_if_t<!std::is_same_v<int, T1>, bool> = true>
TestClass()
{
//balabala
}
......
};
int main() {
TestClass<double> test;
}
我说,这可能是那个不懂C++的人写出来的吧,看,写成下面的形式多简洁阿~
cpp
template<class T>
class TestClass
{
public:
TestClass(){}
//函数3
template<std::enable_if_t<std::is_same_v<int, T>, bool> = true>
TestClass()
{
}
//函数4
template<std::enable_if_t<!std::is_same_v<int, T>, bool> = true>
TestClass()
{
}
};
int main() {
TestClass<double> test;
}
于是就被打脸了,直接给我整出了一堆编译错误:
给我整一脸懵逼,上面函数1和2好好的,咋改成3和4就不行了呢? 看来,真的不能随便改别人代码。于是去网上查了查, 编译器错误 C2893 | Microsoft Learn
cpp
// C2893.cpp
//以下示例生成 C2893。
// compile with: /c /EHsc
#include<map>
using namespace std;
class MyClass {};
template<class T>
inline typename T::data_type
// try the following line instead
// inline typename T::mapped_type
f(T const& p1, MyClass const& p2);
template<class T>
void bar(T const& p1) {
MyClass r;
f(p1,r); // C2893
}
int main() {
map<int,int> m;
bar(m);
}
发生 C2893 的原因是,f 的模板参数 T 被推断为 std::map<int,int>,但 std::map<int,int> 没有成员 data_type(无法使用 T = std::map<int,int> 实例化 T::data_type)。
看来编译错误产生的原因是 std::enable_if_t<std::is_same_v<int, double>, bool>即 std::enable_if_t<false, bool>是未定义类型导致的。可是函数1和函数3到底有什么区别呢? 绞尽脑汁,百思不得其解,最痛通过找不同的方式,终于悟了:这两个唯一的差别就是,T是类模板参数,T1是类构造函数模板参数。后面回顾了一下Sfiane相关的知识,终于找到问题的根本原因:
SFINAE 原理
SFINAE(Substitution Failure Is Not An Error)是 C++ 模板机制的一部分,当模板参数替换导致的模板不合法时,模板不会引发编译错误,而是会被编译器静默排除。然而,SFINAE 只适用于函数模板参数替换 阶段,而不适用于非模板参数替换阶段的错误。
我们先来看一下函数3
cpp
template<std::enable_if_t<std::is_same_v<int, T>, bool> = true>
TestClass()
{
}
在这个函数模板中:
std::enable_if_t<std::is_same_v<int, T>, bool> = true
是模板参数的默认参数。编译器会在函数模板实例化时尝试解析这部分默认参数。
std::enable_if_t<std::is_same_v<int, T>, bool>
依赖于T
,它是类模板TestClass<T>
的参数。在 类模板实例化时,编译器已经需要评估这个表达式来确定默认值是否有效。- 如果
T
不等于int
(例如TestClass<double>
),std::is_same_v<int, T>
变成false
,这时std::enable_if_t<false, bool>
试图生成一个无效类型,这会导致编译错误,而不是被 SFINAE 排除。
SFINAE 只作用于模板参数替换期间产生的错误,而这个默认参数的实例化所依赖的类型T属于类模板参数,不属于该函数模板参数替换阶段。这意味着:
- 编译器在遇到默认参数时需要立即评估它,而不是等到参数替换期间再进行评估。
- 因此,如果默认参数表达式在定义时无效(比如
std::enable_if_t<false, bool>
),编译器会报错,而不是通过 SFINAE 排除它。
我们再来看一下函数1
cpp
template<class T1 = T, std::enable_if_t<std::is_same_v<int, T1>, bool> = true>
TestClass(int i)
{
}
在函数1中,T1
是一个新的模板参数,并且默认值为 T
。因此,SFINAE 是在模板参数 T1
替换阶段应用的:
- 如果
T1
不满足std::is_same_v<int, T1>
,则该模板的实例化会失败,SFINAE 会将此构造函数排除在重载集合之外。
这里的关键在于:
T1
是函数模板的一个参数,所以std::enable_if_t
检查是在模板参数替换阶段发生的。- 如果替换导致无效,则会被 SFINAE 静默排除,不会报编译错误。
这个例子中通过引入额外的模板参数(如 T1
),你可以推迟 enable_if
的检查,使其在模板参数替换阶段才进行,从而避免编译错误。