在模板编程中,有几个常用的技术:模板(偏)特化,特性萃取,标签分派,匹配失败不是错误。其中模板(偏)特化是基础,匹配失败不是错误(SFINAE)应用最为广泛。
现代C++对模板编程做了更多的加强,boost.hana又结合constexpr和lambda把类型与值的计算统一了起来。放眼C++世界,尤其是C++库,几乎都是使用模板的泛型编程。
话说在C++的世界中(并且几乎所有语言中)函数的作用是最为明显的,试想:没有类也可以完成编程任务,但没有函数却不好说。另一方向只使用变量和语句也能完成少量的工作,但不进行函数的封装终难成大事。这几年函数式编程赿来赿流行,与函数的重要性不无关系。所以无论是用类也罢,用函数也罢,或者使用C++新增的变量模板,我们的思路是始终围绕着把它们向函数上靠就好。从MPL的元函数开始就试图使类尽可能像函数,我们在模板编程中也要使模板类、模板变量、乃至模板函数都向“函数”靠拢。这里函数加上引号我想表达的是包括但不限于constexpr函数。
一、先从类型计算将用类实现来转为用函数实现起。将类型计算与值计算统一起来,以例子说话:
1 #include <type_traits> 2 3 //1、先定义一个辅助类,为了将类型表示为对象 4 template<typename T> 5 struct type_t 6 { 7 using type = T; 8 }; 9 10 //2、装饰类型,这里给类型添加指针 11 template<typename T> 12 constexpr auto add_pointer(type_t<T>) 13 { 14 return type_t<T*>{}; 15 } 16 17 //3、为测试提供支持 18 template<typename T> 19 constexpr auto is_pointer(type_t<T>) 20 { 21 return std::false_type{}; 22 } 23 24 template<typename T> 25 constexpr auto is_pointer(type_t<T*>) 26 { 27 return std::true_type{}; 28 } 29 30 //4、测试 31 type_t<int> int_t{}; 32 auto p = add_pointer(int_t); 33 34 auto is_ptr = is_pointer(p); 35 static_assert(is_ptr.value,""); 36 37 int main() 38 { 39 return 0; 40 }
我们没有用元函数式方法实现,因为STL库中实现好了。现在用函数的方式也可以实现了,仔细体会代码,函数把类型和值的计算统一了起来,看起来函数的作用更加强大了。针对以上例子,我们需要类型的时候可以这样
1 // --vs2017rc仍然不能编译-- 2 //template<typename T> 3 //using point_type =typename decltype(add_pointer(type_t<T>{}))::type; 4 5 using point_type = typename decltype(p)::type;
稍嫌繁琐,但可以进一步封装。
1 //将以上代码装到namespace中 2 namespace detail { 3 //... 4 } 5 6 template<typename T> 7 uisng add_pointer_t = typename decltype(detail::add_pointer(type_t<T>{}))::type;
二、把类函数化。还是以例子说话:
1 struct test {}; 2 3 //1、古老的仿函数 4 template<typename T> 5 struct func 6 { 7 template<typename... Args> 8 auto operator()(Args&&... args) const 9 { 10 return T{ std::forward<Args>(args)... }; 11 } 12 }; 13 14 func<test> func_c; 15 auto test_t=func_c(); 16 17 //2、元函数,就不写例子了,STL中很多
func明明是类,但我们把它用出了函数的感觉有没有。
三、把变量模板函数化
上面的代码感觉用起来还是不爽,主要是因为还要先生成一个类对象,我们把它也封装起来,再看个例子
1 struct test2 2 { 3 explicit test2(int x) 4 : x_(x) 5 { 6 7 } 8 9 int x_; 10 }; 11 template<typename T> 12 constexpr func<T> func_c{}; 13 14 auto test2_t = func_c<test2>(1);
看起来就像在调用函数了吧。
总之,不管是什么,只管往函数上靠就对了。
本来想要写sfinae来着,却强烈地想函数的事,现在记录一种利用sfinae的方式吧。
首先,当然是模板特化了。特化前先说说形式完整性这个概念:
有一个别名声明:
1 template<typename...> 3 using void_t=void;
这个别名声明的意思是不管你给我传多少个模板参数,可能是有效的参数我都统统把它们当做void,除非任意一个模板参数不是有效的,那样的话另说(通常是编译器会给你点颜色看看)。
今天我就是要给你无效参数了怎么地?哼哼,我有sfinae大法在手,给我脸色我还不尿你呢!
有了sfinae,编译器会妥协,不管模板参数有效无效它都不敢给你脸色啦。
嗯,我想想哈,举个标签的例子吧。设想是如果一个类有标签,那么我们获取它的标签,如果没有,就什么也不做。
1 template<typename T, typename = void> 2 struct tag_of; 3 4 template<typename T> 5 struct tag_of<T, void_t<typename T::tag>> 6 { 7 using type = typename T::tag; 8 }; 9 10 template<typename T> 11 using tag_of_t = typename tag_of<T>::type; 12 13 template<typename T, typename Tag> 14 struct is_tagof : std::false_type 15 { 16 17 }; 18 19 template<typename T> 20 struct is_tagof<T, tag_of_t<T>> : std::true_type 21 { 22 23 };
模板(偏)特化会优先得到解析。上面is_tagof判断类型T是否有嵌入的tag,如果有,is_tagof就是true_type,如果没有的话,编译器在匹配void_t<typename T::tag>会失败,编译器不认为这是错误而是回头匹配主模板,is_tagof就是false_type