目录
C++ 萃取技术——使用SFINAE特性的信息萃取
SFINAE(Substitution Failure Is Not An Error,替换失败并不是一个错误)是实现类型萃取中一个重要的技巧,它允许编译器在模板实例化过程中忽略那些会导致错误的替换,而不是产生编译错误。
SFINAE 原理:
SFINAE 允许在模板解析过程中,如果某个模板参数不满足某种条件(导致替换失败),则这种失败状态不会导致编译错误,而是使得该模板候选被丢弃。这使得程序员可以设计出多个模板重载或特化,根据不同的类型特性选择最合适的实现。
1. 用成员函数重载实现 is_default_constructible
C++ 标准库提供的可变参类模板
std::is_default_constructible
。这个类模板的主要功能是判断一个类的对象是否能被默认构造(所谓默认构造,就是构造一个类对象时,不需要给该类的构造函数传递任何参数)。
例如,考虑下面两个类:类A可默认构造,而类B则不可:
class A{};
class B
{
public:
B(int tmpvale){}
};
利用标准库提供的可变参类模板std::is_default_constructible
,可以检测各类类型是否可默认构造如下:
#include <iostream>
int main()
{
std::cout << std::is_default_constructible<int>::value << std::endl; // 1 能被默认构造
std::cout <<std::is_default_constructible<double>::value << std::endl; // 1 能被默认构造
std::cout <<std::is_default_constructible<A>::value << std::endl; // 1 能被默认构造
std::cout <<std::is_default_constructible<B>::value << std::endl; // 0 不能被默认构造
return 0;
}
从结果中可以看到,int、double等基本类型(内部类型)对象以及类A对象都是可以默认构造的(结果为1),而类B对象因为其构造函数带一个形参(该形参没有默认值),所以无法默认构造。
这里,将书写一个名为IsDefConstructible
的类模板,完成与std::is_default_constructible
同样的功能,借此看一看如何使用 SFINAE 特性萃取一些重要信息(这里要萃取的信息是判断某个类是否“没有构造函数或有一个不带参数的构造函数”,满足这个条件的类就能够默认构造)。IsDefConstructible
类模板的代码如下:
#include <iostream>
// 用于判断两个类型是否相同 默认情况下返回 false_type
template<typename T1, typename T2>
struct IsSameType : std::false_type {};
// 如果两个类型相同,则特化版本返回 true_type
template<typename T1>
struct IsSameType<T1, T1> : std::true_type {};
class A{};
class B
{
public:
B(int tmpvale){}
};
// 模板类 IsDefConstructible,用于检测类型T是否具有默认构造函数
template<typename T>
class IsDefConstructible
{
private:
// 使用 std::declval<T>() 来避免潜在的构造函数调用
// 探测能否使用默认构造函数创建类型U的实例,若可以,则返回 true_type
template<typename U = T, typename = decltype(U())>
static std::true_type test(void*);
// Fallback函数,如果上述探测失败,则选择此重载,返回 false_type
template<typename = int>
static std::false_type test(...);
public:
// 根据 test(nullptr) 的返回类型,通过IsSameType来确定是否和std::true_type类型相同
// 若相同,则说明T类型可默认构造,value为true;否则为false
static constexpr bool value = IsSameType<decltype(test(nullptr)), std::true_type>::value;
};
int main()
{
// 测试不同类型是否具有默认构造函数
std::cout << IsDefConstructible<int>::value << std::endl; // 1 能被默认构造
std::cout << IsDefConstructible<double>::value << std::endl; // 1 能被默认构造
std::cout << IsDefConstructible<A>::value << std::endl; // 1 能被默认构造
std::cout << IsDefConstructible<B>::value << std::endl; // 0 不能被默认构造
return 0;
}
代码解析:
- 双重测试函数:
IsDefConstructible
包含两个静态成员函数模板test()
。第一个试图通过decltype(T())
来实例化T
,如果成功则返回std::true_type
。第二个是一个包含省略号参数的 Fallback 函数,当第一个测试失败时被调用,返回std::false_type
。 - 优先级和选择机制:编译器首选具体形参的
test()
版本。只有当第一个版本由于 SFINAE 失败时,才会考虑使用省略号形参的版本。 - 静态成员变量
value
:它的值取决于哪个test()
函数被选择。如果decltype(test(nullptr))
的计算结果是std::true_type
,则value
为true
,表示类型T
可以默认构造;否则为false
。
2. 用成员函数重载实现 is_convertible
C++ 标准库中提供了
std::is_convertible
类模板,它主要用于判断是否可以从一个类型隐式转换到另一个类型,并返回布尔值true
或false
。
例如,通常可以从 int
转换到 float
,反之亦然。再比如,给定两个类 A
和 B
,其中 B
继承自 A
,因此可以从 B
转换到 A
,但反过来从 A
转换到 B
则不可行。以下是相关代码示例:
#include <iostream>
class A {};
class B : public A {};
int main()
{
std::cout << std::is_convertible< float, int>::value << std::endl; // 1 可以转换
std::cout <<std::is_convertible<int, float>::value << std::endl; // 1 可以转换
std::cout << std::is_convertible<A,B>::value << std::endl; // 0 不可以转换
std::cout << std::is_convertible<B, A>::value << std::endl; // 1 可以转换
return 0;
}
接下来,通过自定义 IsConvertibleHelper
类模板和 IsConvertible
类模板来实现与 std::is_convertible
相同的功能。注意保持类型模板参数的顺序:第一个参数表示源类型(FROM),第二个参数表示目标类型(TO)。IsConvertibleHelper
类模板的作用是检测从类型 FROM 到类型 TO 的转换是否可行。
#include <iostream>
class A {};
class B : public A {};
template<typename FROM, typename TO>
struct IsConvertible
{
private:
// 尝试使用类型TO的参数来测试是否可以从FROM转换到TO
static void test(TO) {}
// 通过decltype和表达式SFINAE检测两种类型的转换函数
template<typename F>
static auto check(F* f) -> decltype(test(*f), std::true_type());
// 如果上面的尝试失败,则使用这个通用函数,返回std::false_type
static std::false_type check(...) {}
public:
// 根据FROM类型是否可以被转换为TO类型,value将是true或false
static constexpr bool value = decltype(check(static_cast<FROM*>(nullptr)))::value;
};
int main()
{
std::cout << "float to int: " << IsConvertible<float, int>::value << std::endl; // 1 可以转换
std::cout << "int to char: " << IsConvertible<int, char>::value << std::endl; // 1 可以转换
std::cout << "A to B: " << IsConvertible<A, B>::value << std::endl; // 0 不能转换
std::cout << "B to A: " << IsConvertible<B, A>::value << std::endl; // 1 可以转换
return 0;
}
详细解释:
test(TO)
- 辅助函数,尝试触发从FROM
到TO
的隐式类型转换。check(F* f)
- 模板辅助函数,尝试调用test(*f)
。如果FROM
类型的对象可以被成功转换为TO
类型,将返回std::true_type
。check(...)
- 如果不能调用test(*f)
(即FROM
不能转换为TO
),则选择这个备用函数,返回std::false_type
。static_cast<FROM\*>
- 在主函数check
中传递一个FROM
类型的指针,以尝试执行类型转换。
3. 用成员函数重载实现 is_class
std::is_class
是 C++11 标准中的一个类模板,用于判断某个类型是否为类类型(但不包括联合类型)。
以下是一个简单的 IsClass
类模板实现,该实现模仿了 std::is_class
的功能。此实现使用成员指针和模板特化来确定一个类型是否为类类型。
#include <iostream>
#include <type_traits>
// 判断两个类型是否相同,默认情况下返回 std::false_type
template<typename T1, typename T2>
struct IsSameType : std::false_type {};
// 如果两个类型相同,则特化版本返回 std::true_type
template<typename T>
struct IsSameType<T, T> : std::true_type {};
class A {};
class B : public A {};
template<typename T>
class IsClass
{
private:
// 尝试声明一个成员指针,仅当 T 是类类型时才有效
template<typename U>
static std::integral_constant<bool, !std::is_union<U>::value> test(int U::*);
// 备选函数,当第一个函数模板不适用时调用
template<typename>
static std::integral_constant<bool, false> test(...);
public:
// 比较两个类型,确认是否为 true 类型
static constexpr bool value = IsSameType<decltype(test<T>(nullptr)), std::integral_constant<bool, true>>::value;
};
int main()
{
std::cout << "Is A a class? " << IsClass<A>::value << std::endl; // 1,A 是类类型
std::cout << "Is B a class? " << IsClass<B>::value << std::endl; // 1,B 是类类型
std::cout << "Is int a class? " << IsClass<int>::value << std::endl; // 0,int 不是类类型
return 0;
}
详细解释:
IsSameType
- 这是一个模板结构,用于比较两个类型。默认情况下,它继承自std::false_type
。当两个类型相同时,通过特化提供了一个继承自std::true_type
的版本。IsClass
- 这个类模板利用 SFINAE 原理判断一个类型是否为类。首先,尝试定义一个只有当类型U
是类时才有效的成员指针类型的模板函数test
。如果T
是类类型且不是联合类型,这个函数将被选中。否则,test(...)
作为通用备选项被调用。value
- 静态成员变量value
利用IsSameType
来确定test<T>(nullptr)
的返回类型是否为std::integral_constant<bool, true>
,从而确定T
是否为类类型。
4. 用成员函数重载实现 is_base_of
std::is_base_of
是 C++11 标准中的一个类模板,用于判断一个类(Base)是否是另一个类(Derived)的基类。
该模板有两个参数:第一个参数表示基类,第二个参数表示派生类。这与 std::is_convertible
不同,后者用于检测是否可以从一个类型转换到另一个类型,并不要求两者有继承关系,如从 float
转换到 int
。
#include <iostream>
class A {};
class B : public A {};
int main()
{
std::cout << std::is_base_of<A, A>::value << std::endl; // 1
std::cout << std::is_base_of<B, A>::value << std::endl; // 0
std::cout << std::is_base_of<A, B>::value << std::endl; // 1
return 0;
}
C++17 标准进一步简化了 std::is_base_of
的使用,引入了变量模板 is_base_of_v
。
template< class Base, class Derived >
inline constexpr bool is_base_of_v =is_base_of<Base, Derived>::value;
因此,主函数的代码可以修改为:
int main()
{
std::cout << std::is_base_of_v<A, A> << std::endl;
std::cout << std::is_base_of_v<B, A> << std::endl;
std::cout << std::is_base_of_v<A, B> << std::endl;
return 0;
}
现在来看一下 IsBaseOf
类模板的实现:
#include <iostream>
// 判断两个类型是否相同,默认情况下返回 std::false_type
template<typename T1, typename T2>
struct IsSameType : std::false_type {};
// 如果两个类型相同,则特化版本返回 std::true_type
template<typename T>
struct IsSameType<T, T> : std::true_type {};
class A {};
class B : public A {};
template<typename Base, typename Derived>
class IsBaseOf
{
private:
// 试图将 Derived* 转换为 Base*,如果成功则选择此重载
template<typename T>
static std::true_type test(T*);
// 如果转换失败,则选择此重载
template<typename>
static std::false_type test(void*);
// 利用 decltype 和 SFINAE 确定 test 函数的返回类型
template<typename B, typename D>
static auto test_middle() -> decltype(test<B>(static_cast<D*>(nullptr)));
public:
// 判断是否是基类关系,需要满足两个条件:
// 1. Base 和 Derived 都是类类型;2. 能够将 Derived* 转换为 Base*
static constexpr bool value =
IsSameType<std::integral_constant
<bool, std::is_class_v<Base> && std::is_class_v<Derived> &&
decltype(test_middle<Base,Derived>())::value>,
std::integral_constant<bool,true>>::value;
};
int main()
{
std::cout << "Is A base of A? " << IsBaseOf<A, A>::value << std::endl; // 1
std::cout << "Is B base of A? " << IsBaseOf<B, A>::value << std::endl; // 0
std::cout << "Is A base of B? " << IsBaseOf<A, B>::value << std::endl; // 1
return 0;
}
详细解释:
IsBaseOf
- 利用 SFINAE 原理,这个类模板检查一个类型是否为另一个类型的基类。test(T*)
和test(void*)
- 这两个成员函数模板尝试推断给定的类型关系。如果Derived
可以被安全地转换为Base*
,则test(T*)
被选中,反之则是test(void*)
。test_middle
- 中介函数,使用static_cast
尝试将Derived*
转换为Base*
,并根据转换结果选择适当的test
函数。value
- 静态成员变量value
结合了类检查和类型比较,确保只有在两个类型都是类且派生关系成立时才返回true
。
5. 用类模板特化实现 is_default_constructible
利用 std::void_t
和模板特化来实现自定义的 IsDefConstructible
类模板。
下面将重新实现IsDefConstructible
类模板的功能,这次的实现代码将用到std::void_t
,而且将采用特化方式实现,代码如下。
#include <iostream>
class A {};
class B : public A
{
public:
B(int tmpval) {};
};
// 泛化版本,默认假设类型不可默认构造
template<typename T,typename U = std::void_t<>>
class IsDefConstructible : public std::false_type {};
// 特化版本,如果可以默认构造 T,则启用此特化
template<typename T>
class IsDefConstructible<T, std::void_t<decltype(T())>> :public std::true_type {};
int main()
{
std::cout << IsDefConstructible<A>::value << std::endl; // 1 能被默认构造
std::cout << IsDefConstructible<B>::value << std::endl; // 0 不能被默认构造
return 0;
}
代码解释:
- 类定义:
A
是一个空类,自动具备默认构造函数。B
继承自A
但只定义了一个接受int
参数的构造函数,没有默认构造函数。
- 模板定义:
- 泛化版本:默认情况下,假设类型
T
不可默认构造,因此继承自std::false_type
。这是一个保守的假设,用于在特化无法应用时的回退情况。 - 特化版本:使用
decltype
和默认构造表达式T()
尝试推导T
的类型,结合std::void_t
来实现 SFINAE。如果T()
是一个合法表达式,则decltype(T())
成功推导类型,使得std::void_t<decltype(T())>
被解析为void
。此时,特化版本有效并继承自std::true_type
,表示类型T
可以被默认构造。
- 泛化版本:默认情况下,假设类型
- SFINAE 原理:
- 对于
IsDefConstructible<A>
:类A
可以被默认构造,因此decltype(A())
是有效的,导致std::void_t<decltype(A())>
成功解析为void
,从而选择这个特化版本,其结果为true
。 - 对于
IsDefConstructible<B>
:类B
无法默认构造(因为缺失默认构造函数),因此decltype(B())
会失败,这种失败由于 SFINAE 原则,不会导致编译错误,而会导致特化版本不被选中。因此,编译器回退到泛化版本,其结果为false
。
- 对于
总结
类型萃取与 SFINAE 经常结合使用来检查类型是否具有某些属性,例如:
- 是否可默认构造
- 是否含有特定的成员函数或类型
- 是否满足特定的表达式有效性
这些检查通常通过设计一些试探性的模板代码来进行,利用 std::void_t
、decltype
等工具检查特定的属性或操作是否有效。