SFINAE(std::enable_if)是C++11模板中利用类型萃取判断泛型是否满足特定鸭子类型,从而保证模板代码能正确地条件化生成的方法,但是语法上可读性差,不易扩展。
而 C++20中的Concept使用不求值表达式表达泛型约束,语法上限制更少,可读性强,结合C++14的if constexpr,可以平替满是语法噪音的 SFINAE。最重要的是,concepts报错能精确到requires,而没有与static_assert的 SFINAE很难定位写模板时的bug
假设一个模板函数内,要根据类型是否具有特定成员变量(函数)使用if constexpr条件编译,比如下方代码需要确定类T是否有public的int x, auto f() -> void
struct A{
double x;
void f(){};
};
struct B{
int x;
void g(){};
};
template <typename T> t_func(T t) {
if constexpr (has_x<T> && has_f<T>)
branch1();
else
branch2();
}
C++20前使用 SFINAE可以这样写:
template<typename T>
struct has_x {
template<typename U, typename = void>
struct has_x_impl : std::false_type {
};
template<typename U>
struct has_x_impl<U, std::void_t<decltype(std::declval<U>().x)>> {
constexpr inline bool static value = std::is_same_v<int, decltype(std::declval<U>().x)>;
};
constexpr inline bool static value = has_x_impl<T>::value;
};
如果T::x存在,则has_x<T>会实例化为没有默认typename = void的第二个版本的has_x_impl,has_x<T>::value当T::x为int时为真
如果T::x不存在,第二个版本的has_x_impl替换失败,根据SFINAE实例化为第一个版本,has_x<T>::value==false
为了实现 SFINAE,使用了:1.一个额外的typename=void用于触发SFINAE 2.decltype用于类型提取 3. 在decltype的不求值表达式中使用std::declval<U>(),防止U没有默认构造函数导致编译错误
判断成员函数的代码类似
template<typename T>
struct has_f {
template<typename U, typename = void>
struct has_f_impl : std::false_type {
};
template<typename U>
struct has_f_impl<U, std::void_t<decltype(std::declval<U>().f())>> {
constexpr inline bool static value = std::is_same_v<void, decltype(std::declval<U>().f())>;
};
constexpr inline bool static value = has_f_impl<T>::value;
};
而在c++20中,使用Concepts将上述满满语法噪音的代码化简为:
template<typename T>
concept has_x = requires(T a) {
requires std::same_as<decltype(a.x), int> ;
};
template<typename T>
concept has_f = requires(T a) {
{a.f()} -> std::same_as<void>;
};
当然has_x也可写成
template<typename T>
concept has_x = requires(T a) {
{a.x} -> std::same_as<int&> ;
};
不写成std::same_as<int>是因为这里和{} ->检查的是表达式a.x的类型,而不是变量a.x本身的类型。编译器转换的代码大致等价于
template<typename T>
concept has_x = requires(T a) {
requires std::same_as<decltype((a.x)), int&> ;
};
requires表达式中从T类型对象中拿出成员,嵌套一层requires检查变量类型/函数签名
使用例
struct A{
double x;
void f(){};
A() = delete;
};
struct B{
int x;
void g(){};
};
int main(){
cout << has_x<A> << endl << has_x<B> << endl;
cout << has_f<A> << endl << has_f<B> << endl;
// 0, 1, 1, 0
}