逻辑运算符元函数
在C++中逻辑运算符可以将两个或多个关系表达式连接成一个,例如&&
和||
,也能够使表达式的逻辑反转,例如!
。在这个小节中,我们将根据C++的逻辑运算符实现一套YAMPL可以使用的逻辑运算符元函数,除此之外我们还将结合上面的内容来完成一个元编程例子。
template <class N1, class N2, class... Nargs> |
观察上面的代码会发现,and_
元函数的实现和plus
元函数几乎相同,除了使用了不同的运算符以外,唯一的区别就是返回类型。and_
元函数的返回类型using type = integral_const<value_type, N1::value && inner::value>;
固定为integral_const<bool, true>
或者integral_const<bool, false>
之一,也就是true_type
或者false_type
。这个设计正好是对应&&
运算符的返回值必须是true
或者false
之一。说明了这个区别之后,读者可以回味一下plus
的实现应该就能理解and_
元函数的实现细节了,这里也不再赘述。
如and_
元函数一样,or_
也可以通过这样的方式实现,而且只需要修改一个运算符而已。所以这里还是用宏简化代码的实现:
|
以上代码实现了逻辑与和逻辑或的运算符元函数,接下来我们需要实现一个逻辑非运算符元函数,也就是在C++中常用的!
。逻辑非运算符元函数的实现相对于前两个就单纯多了,代码如下:
template <class T> |
这里只需要注意not_
元函数的返回类型也是固定为integral_const<bool, true>
或者integral_const<bool, false>
之一,剩下的代码和取负运算符元函数的代码几乎相同很容易理解。
现在,我们要利用上面介绍的内容实现一个特殊的函数模板:
template<class T> |
该函数模板需要完成这样一个任务:当模板实参T
是一个标量或者引用时,函数参数的T
为实参本身;否则T
为实参的引用。也就是说当T
为int
时,函数为void special_func(int) {}
;当T
为int&
时,函数为void special_func(int&) {}
;当T为std::string
时,函数为void special_func(std::string&) {}
。
以下是我的一种实现方案:
template <class T> |
上面的代码中实现了一个func_helper
元函数,该元函数会针对special_func
的模板实参进行处理以满足函数需求。在func_helper
的实现代码中,首先调用了逻辑或元函数or_
,用于判断T
是否为标量或者引用类型。然后根据返回结果调用if_
元函数。当cond::type
的结果为true_type
时返回T
本身,否则调用std::add_lvalue_reference
返回T
的引用类型,最终type
为要求的返回类型。
来测试一下刚刚编写的函数模板:
int n = 1; |
使用-fdump-tree-gimple
命令让GCC生成gimple的中间代码,观察代码发现这样三份中间代码:
special_func<int> (type v) |
可以看到当模板实参为int
和int&
时,函数形参类型为模板实参本身,而当模板实参为std::__cxx11::basic_string<char>
时,函数形参类型为struct basic_string &
,满足函数的设计要求。
类型打印元函数
模板元程序之所以比普通C++程序更难编写主要是因为它很难调试。我们常用的调试方法在模板元程序上都没法正常使用,比如调试器只能调试动态运行的程序,但是却无法调试编译期执行的元程序。
另外打印日志的方法也许能帮上一点忙,因为C++为我们提供了typeid
这个操作符,它返回的std::type_info
结构中存在一个const char* name()
的成员函数可以返回类型名称。于是我们想到可以使用以下方法打印类型信息帮助调试:
std::cout << typeid(T).name(); |
不过遗憾的是,这种方法也并不完美。首先来说,成员函数name()
返回的类型名称在不同编译器中有不同的展现方法,比如MSVC编译出来的程序返回的是一个可读的名称,而GCC编译出来的程序返回的类型名称则需要使用特定API(例如abi::__cxa_demangle
)将其转换为可读的名称。其次,typeid
也无法真实的反应类型的状态,因为C++标准中说明了typeid
会忽略类型的cv
属性,也就是说typeid(const T) == typeid(T)
。所以typeid
打印日志的方法也不满足需求。
为了准确是输出类型信息,我们需要将目光从程序本身移动到编译期上,因为只有编译期才是掌握类型信息最全面的程序。于是我们可以想到使用编译期的错误信息来打印类型信息。由于错误信息往往是帮助程序员排查错误,所以类型信息会非常的全面。
template <class T> |
在上面的代码中err_print_type
是一个缺少实现的类模板,所以当编译器将其进行实例化的时候必然会报错,而错误信息正是我们想要的结果。err_print_type<typename minus<int_<10>, int_<2>>::type>();
在MSVC中会显示错误信息:
error C2027: use of undefined type 'err_print_type<yampl::integral_const<T,8>>' |
在GCC中显示错误信息:
error: invalid use of incomplete type 'struct err_print_type<yampl::integral_const<int, 8> >' |
可以看到,无论是哪种编译器都非常详细的显示了类型信息。
现在类型信息是完整了,但这种方法还是不太好,因为错误会阻止程序的编译导致无法生成可执行程序。我们需要一种方法既能在编译期产生可用的日志,与此同时也不能阻碍程序的正常编译。于是我们想到,如果能将错误信息转换为警告信息不久好了么!在YAMPL中,打印类型信息的方法就是用这种思路实现的。
|
以上代码实现了一个类模板dbg_print_type
,并且分别对MSVC、GCC和CLang做了支持。当编译期时MSVC时,使用了数组大小为0的技巧促使编译期发出警告;当编译器是CLang时,使用除数为0的方式让编译器发出警告;当编译器是GCC时,使用不同符号类型比较让编译器发出警告,值得注意的是这个警告需要手动开启。
将上面示例中的err_print_type
修改为dbg_print_type
:
dbg_print_type<typename minus<int_<10>, int_<2>>::type>(); |
GCC会发出这样的警告:
In instantiation of 'struct yampl::DbgPrintType<yampl::integral_const<int, 8> >': |
MSVC显示的警告为:
warning C4200: nonstandard extension used: zero-sized array in struct/union |
请注意警告的信息确实比较多,但是仔细观察还是能看到'struct yampl::DbgPrintType<yampl::integral_const<int, 8> >'
这样类似的信息。另外值得高兴的是,这些警告信息也确实没有阻止程序的编译。