C++ 萃取技术——使用SFINAE特性的信息萃取

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;
}

代码解析:

  1. 双重测试函数IsDefConstructible 包含两个静态成员函数模板 test()。第一个试图通过 decltype(T()) 来实例化 T,如果成功则返回 std::true_type。第二个是一个包含省略号参数的 Fallback 函数,当第一个测试失败时被调用,返回 std::false_type
  2. 优先级和选择机制:编译器首选具体形参的 test() 版本。只有当第一个版本由于 SFINAE 失败时,才会考虑使用省略号形参的版本。
  3. 静态成员变量 value:它的值取决于哪个 test() 函数被选择。如果 decltype(test(nullptr)) 的计算结果是 std::true_type,则 valuetrue,表示类型 T 可以默认构造;否则为 false

2. 用成员函数重载实现 is_convertible

C++ 标准库中提供了 std::is_convertible 类模板,它主要用于判断是否可以从一个类型隐式转换到另一个类型,并返回布尔值 truefalse

例如,通常可以从 int 转换到 float,反之亦然。再比如,给定两个类 AB,其中 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;
}

详细解释:

  1. test(TO) - 辅助函数,尝试触发从 FROMTO 的隐式类型转换。
  2. check(F* f) - 模板辅助函数,尝试调用 test(*f)。如果 FROM 类型的对象可以被成功转换为 TO 类型,将返回 std::true_type
  3. check(...) - 如果不能调用 test(*f)(即 FROM 不能转换为 TO),则选择这个备用函数,返回 std::false_type
  4. 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;
}

详细解释:

  1. IsSameType - 这是一个模板结构,用于比较两个类型。默认情况下,它继承自 std::false_type。当两个类型相同时,通过特化提供了一个继承自 std::true_type 的版本。
  2. IsClass - 这个类模板利用 SFINAE 原理判断一个类型是否为类。首先,尝试定义一个只有当类型 U 是类时才有效的成员指针类型的模板函数 test。如果 T 是类类型且不是联合类型,这个函数将被选中。否则,test(...) 作为通用备选项被调用。
  3. 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;
}

详细解释:

  1. IsBaseOf - 利用 SFINAE 原理,这个类模板检查一个类型是否为另一个类型的基类。
  2. test(T*)test(void*) - 这两个成员函数模板尝试推断给定的类型关系。如果 Derived 可以被安全地转换为 Base*,则 test(T*) 被选中,反之则是 test(void*)
  3. test_middle - 中介函数,使用 static_cast 尝试将 Derived* 转换为 Base*,并根据转换结果选择适当的 test 函数。
  4. 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;
}

代码解释:

  1. 类定义
    • A 是一个空类,自动具备默认构造函数。
    • B 继承自 A 但只定义了一个接受 int 参数的构造函数,没有默认构造函数。
  2. 模板定义
    • 泛化版本:默认情况下,假设类型 T 不可默认构造,因此继承自 std::false_type。这是一个保守的假设,用于在特化无法应用时的回退情况。
    • 特化版本:使用 decltype 和默认构造表达式 T() 尝试推导 T 的类型,结合 std::void_t 来实现 SFINAE。如果 T() 是一个合法表达式,则 decltype(T()) 成功推导类型,使得 std::void_t<decltype(T())> 被解析为 void。此时,特化版本有效并继承自 std::true_type,表示类型 T 可以被默认构造。
  3. SFINAE 原理
    • 对于 IsDefConstructible<A>:类 A 可以被默认构造,因此 decltype(A()) 是有效的,导致 std::void_t<decltype(A())> 成功解析为 void,从而选择这个特化版本,其结果为 true
    • 对于 IsDefConstructible<B>:类 B 无法默认构造(因为缺失默认构造函数),因此 decltype(B()) 会失败,这种失败由于 SFINAE 原则,不会导致编译错误,而会导致特化版本不被选中。因此,编译器回退到泛化版本,其结果为 false

总结

类型萃取与 SFINAE 经常结合使用来检查类型是否具有某些属性,例如:

  • 是否可默认构造
  • 是否含有特定的成员函数或类型
  • 是否满足特定的表达式有效性

这些检查通常通过设计一些试探性的模板代码来进行,利用 std::void_tdecltype 等工具检查特定的属性或操作是否有效。

  • 19
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值