最近在看gem5代码的时候发现里面一大堆神奇操作。由于并没有系统地学过C++,导致看别人代码的时候先是一头问号,好容易弄懂之后又只得拍手称绝。这里介绍一下C++11引入的模板逻辑std::enable_if

enable_if包含于type_traits头文件中,本质上是一个结构体,但是通过借用C++模板的特性实现了逻辑功能,因而我更倾向于把它按照功能称作逻辑,而非按照其在语言中的成员类型将其称为结构体/类型。

enable_if本身不算是语言特性,也不是复杂的软件实现,可以说仅仅只算一种语法糖。其功能大致可以概括为,如果模板类中的第一个参数为true的话,按照第二个参数生成一个有效的数据类型定义;为false的话则产生编译报错。这种功能可以允许在C++模板编程的时候,按照enable_if的条件变量决定所指定的函数或变量是否在指定条件的类中定义。

为了理解enable_if的工作原理,需要先了解C++模板类中bool类型参数的作用。简单来说,可以当作模式匹配的参数使用:

template<bool B>
class A
{
public:
    const bool opt = B;
};

对于类A则有两种可能的实例化情况,分别是A<true>()A<false>()。由此,可以分别为这两种模式下的范型定义不同类型的逻辑和代码:

template<>
class A<true>
{
public:
    const bool opt = true;
};

template<>
class A<false>
{
public:
    const bool opt = false;
};

这个时候访问A<true>().optA<false>().opt,则可以实例化为指定的类,读取到opt的值分别为truefalse

然后回来看enable_if的实现。cppreference上给出的参考实现如下:

template<bool B, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> { typedef T type; };  // 有的实现里用的是`using type = T;`,效果相同

可以看见,enable_if只需要四行代码就可以完成功能,但是如果要求要使用的人在每次使用的时候自己写,又显得太麻烦。可能就是出于代码可读性的目的放进了头文件中吧。

这里可以看见的是,与上文不同的地方在于,这里定义完enable_if<bool, T>的原型之后,只定义了enable_if<true, T>类型,而没有定义enable_if<false, T>类型。这样做的后果是,如果在后续过程中使用到了enable_if<false, T>类型的话,编译器会报出类似invalid use of incomplete type的错误,示意所使用的类型并没有完成显式声明。为了避免这个问题,enable_if巧妙地在类名后添加了一组花括号,示意默认实现是一个没有任何成员的空类。这样就确保了类是完整的,同时避免了类中夹杂不需要的属性。

那么,此时访问enable_if<true, T>::type的话,是可以获取到type所指类型T的,而访问enable_if<false>::type的话,虽然类型是完整的,但如上文所述,会因为访问没有定义的成员类型而在编译阶段被编译器当作错误报出。举一个简单的例子:

#include <type_traits>

...

    std::enable_if<false, void>::type t;

在编译阶段,编译器会指出这一行存在问题,因为在enable_if的首个参数为false的情况下,是没有定义type成员类型的:

error: 'type' is not a member of 'std::enable_if<false, void>'

但是如果放在这个模板逻辑的应用场景中,却又不会报错:

template<bool B>
class A
{
public:
    typename std::enable_if<B, bool>::type isTrue()
    {
        return true;
    }
};

使用这段代码声明模板类AA<true>()A<false>()都可以正常编译通过,但是只有A<true>()可以访问成员方法isTrue()enable_if的目的达到了,但是在实例化A<false>()的时候显然是遇到了typename std::enable_if<false, bool>::type isTrue()这个情况的。编译器并没有报错,而是选择在不实现这个函数的同时,正常完成剩余部分的编译。

编译器的这种行为与C++的一种新兴提出的C++编译思想,SFINAE有着极大的关系。SFINAE是英文短句Substitution Failure Is Not An Error(匹配失败不是(编译)错误)的缩写。SFINAE的一种体现是,在模板类中使用推导类型构建类方法的时候,如果出现了匹配错误(无符合匹配或多重匹配)的话,选择不报错,而是直接放弃对该模板函数的实例化操作。这也正是enable_if能够实现的核心原理:一般编码情况下,访问到enable_if<false, T>::type的话会报出类型没有该成员的错误;而在模板类编程中,由于找不到enable_if<false, T>::type对应的数据类型,根据SFINAE规则,编译器会选择放弃这种情况下对该函数的实现(而不是报错说找不到匹配类型),进而完成后续的编译工作,生成正常可用的程序。

参考:
https://zhuanlan.zhihu.com/p/20029261
https://ouuan.github.io/post/c-11-enable-if-%E7%9A%84%E4%BD%BF%E7%94%A8/
https://en.cppreference.com/w/cpp/types/enable_if
https://en.cppreference.com/w/cpp/language/sfinae

标签: none

添加新评论