【C++】Concept, SFINAE判断类是否有特定非静态成员

SFINAEstd::enable_if)是C++11模板中利用类型萃取判断泛型是否满足特定鸭子类型,从而保证模板代码能正确地条件化生成的方法,但是语法上可读性差,不易扩展。

C++20中的Concept使用不求值表达式表达泛型约束,语法上限制更少,可读性强,结合C++14if constexpr,可以平替满是语法噪音的 SFINAE。最重要的是,concepts报错能精确到requires,而没有与static_assertSFINAE很难定位写模板时的bug

假设一个模板函数内,要根据类型是否具有特定成员变量(函数)使用if constexpr条件编译,比如下方代码需要确定类T是否有publicint 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_implhas_x<T>::valueT::xint时为真

如果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
}