现代C++之”模板元编程”
从17年开始就不太做C++开发了,因此知识还停留在C++03上,所以这次趁着过年的短暂空隙补一下比较有意思的现代C++特性,有些简单的新语法不是我想表达的重点就一笔带过了。
本文通过1个简单例子来了解”模板元编程”,代码:https://github.com/owenliang/modern-cpp/blob/main/meta_func.cpp。
模板元编程(template meta programming)不是什么新东西,但是现代C++已经把这些黑科技放到了台面上广泛使用,所以我们看看。
模板元编程的2个例子
模板元编程的核心就是template struct,目的是在编译阶段对类型进行运算(相比较于代码运行时进行值运算来说)。
比如,下面的_is_same结构体就是一个模板元编程的例子,它可以判断2个类型是不是一样的:
1 2 3 4 5 6 7 8 9 10 |
// meta function 1:用于判断2个类型是否一样的struct template<typename T1, typename T2> struct _is_same { static const bool value = false; }; template<typename T> struct _is_same<T, T> { static const bool value = true; }; |
特化版本要求T1和T2是同类型的,此时value属性为true,表示T1和T2同类型。
可以像这样来比较2个类型:
1 2 3 4 5 |
int main(int argc, char** argv) { std::cout << _is_same<int, double>::value << std::endl; std::cout << _is_same<int, int>::value << std::endl; return 0; } |
打印0和1,也就是false和true,显然模板元编程运算和比较的是类型,其手段是基于template struct和特化的方式来实现的。
再看第2个例子,
1 2 3 4 5 6 7 8 9 10 |
// meta function 2:用于断言,如果为真则可以取到::type,否则取不到 template<bool, typename T> struct _enable_if { using type = T; }; template<typename T> struct _enable_if<false, T> { // 不定义type, 那么尝试获取type的代码将被SFINAE(Substitution failure is not an error) }; |
_enable_if的第1个模板参数固定为bool类型,可以传true or false。
当bool为true时,那么type就是T;当bool为false时,_enable_if特化版本没有type;这到底是在干什么呢?这其实意味着:
1 2 3 4 5 |
int main(int argc, char** argv) { _enable_if<true, int>::type i = 5; std::cout << i << std::endl; return 0; } |
相当于int i = 5,因为_enable_if<true, …>的时候结构体内using type = int,所以::type就是int。
那么传false分析一下:
1 2 3 4 5 |
int main(int argc, char** argv) { _enable_if<false, int>::type i = 5; std::cout << i << std::endl; return 0; } |
编译会失败报错:
1 2 |
meta_func.cpp:94:29: error: ‘type’ is not a member of ‘_enable_if<false, int>’ _enable_if<false, int>::type i = 5; |
_enable_if<false, int>结构体内没有type(偏特化版本就是没有啊~),所以失败理所当然。
似乎_enable_if能实现类似断言的感觉,关键这么干有啥用呢?我们再结合一下_enable_if和_is_same看一下:
1 2 3 4 5 |
int main(int argc, char** argv) { _enable_if<_is_same<double, double>::value, int>::type i = 5; std::cout << i << std::endl; return 0; } |
这段代码可以编译运行:因为double和double一样,所以_is_same<double,double>::value是true,所以_enable_if<true,int>::type就是int。
因此,如果_is_same<T1,T2>类型不一样的话,那么这段代码就会编译失败,因为_enable_if<false>取不到type。
好的,其实_enable_if不只是为了让我们false时编译不过而设计的!它有更重要的使命。
使用_enable_if和_is_same实现有用的功能!
现在我们实现_distance1函数,它可以求2个迭代器之间的距离。
1 2 3 4 5 |
std::vector<int> vec{1,2,3,4,5}; size_t d1 = _distance1(vec.begin(), vec.begin() + 3); std::map<std::string, int> m{{"a", 10}, {"b", 11}, {"c", 12}, {"d", 13}}; size_t d2 = _distance1(m.find("a"), m.find("d")); |
这里必须补充一个关键知识,就是vector的迭代器是随机迭代器(内存空间连续),所以支持用iter2-iter1求出距离;而map的迭代器无法直接做减法求距离(因为是树结构),所以只能while(iter1++ != iter2)的方式迭代计算距离。
所以我们期望_distance1最好能够判断出迭代器是不是随机迭代器,如果是的话可以直接iter2-iter1得到距离,否则再走低效率的++方式。
c++标准库保证每个迭代器类型内部都有一个叫做iterator_category的typedef来定义它是什么类型的迭代器(随机迭代器、单向迭代器、双向迭代器…),既然能拿到迭代器的类型,那么再结合一些元编程的类型计算能力就可以实现基于类型控制代码逻辑的能力了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// distance版本1:基于if constexpr实现分支处理 template<typename Iter> size_t _distance1(Iter s, Iter e) { // 迭代器的类型 using category = typename std::iterator_traits<Iter>::iterator_category; // constexpr if在编译器执行, 完成对代码的裁剪 if constexpr (_is_same<category, std::random_access_iterator_tag>::value) { std::cout << "_distance1 版本A" << std::endl; return e - s; } else { size_t d = 0; while (s != e) { ++s; ++d; } std::cout << "_distance1 版本B" << std::endl; return d; } } |
迭代器类型是Iter,先利用iterator_traits拿到Iter的迭代器类型。
然后_is_same判断它是不是std::random_access_iterator_type,取_is_same<>::value就是true or false了,这个bool值在编译阶段就已经明确了,所以当Iter是随机迭代器时if实际上就是if (true) {},否则就是if (false)。
在前一篇博客我们讲过if constexpr可以在编译时直接根据if裁剪掉无效代码分支,所以当_is_same<>::value是true时代码直接就是return e-s求出距离,否则只会保留else部分的代码来满足非随机迭代器的距离计算。
再来看_distance2的实现,它充分体现了模板元编程的关键实践思路:
1 2 3 4 5 6 |
// distance版本2:模板参数+SFINAE template<typename Iter, typename _enable_if<_is_same<typename std::iterator_traits<Iter>::iterator_category, std::random_access_iterator_tag>::value, int>::type = 0> size_t _distance2(Iter s, Iter e) { std::cout << "_distance2 版本A" << std::endl; return e - s; } |
迭代器是Iter类型,我们做了一个_enable_if来判断Iter是不是随机迭代器,如果_is_same为true的话则_enable_if<true, int>::type不会报错并且此时type是int,我们为int模板参数定一个没意义的默认值0,我们传vector的迭代器进来则编译不会报错:
1 2 |
std::vector<int> vec{1,2,3,4,5}; size_t d1 = _distance2(vec.begin(), vec.begin() + 3); |
但如果我们传入的Iter是map的迭代器呢?_is_same<>::value肯定是false,那么_enable_if<false, int>::type就取不到type导致报错,代码就无法编译成功了,难道map迭代器就不让用了??
我们只需要额外实现另一个_distance2即可:
1 2 3 4 5 6 7 8 9 10 |
template<typename Iter, typename _enable_if<!_is_same<typename std::iterator_traits<Iter>::iterator_category, std::random_access_iterator_tag>::value, int>::type = 0> size_t _distance2(Iter s, Iter e) { size_t d = 0; while (s != e) { ++s; ++d; } std::cout << "_distance2 版本B" << std::endl; return d; } |
注意区别就是_is_same前面加了一个叹号,那么只要迭代器类型不是random的,那么_is_same<>::value就是false,那么!_is_same<>::value就是true,那么_enable_if<true, int>::type就不会报错(能取到type int),那么C++编译器就会选择该版本的_distance2来实例化,并且忽略掉第1个_distance2的_eanble_if<>::type取不到的错误。
1 2 |
std::map<std::string, int> m{{"a", 10}, {"b", 11}, {"c", 12}, {"d", 13}}; size_t d2 = _distance2(m.find("a"), m.find("d")); |
我们把C++编译器寻找与尝试实例化模板函数过程中能够忽略掉语法不成立的错误的机制叫做:
替换失败并非错误 (Substitution failure is not an error),简写sfinae。
我们之所以要做template <Iter, int=0>中的int=0参数,完全就是为了让_enable_if能够有个地方让编译器进行类型计算(sfinae)而已,其实这个int与函数distance的功能完全没任何关系。
这个机制对模板编程太重要了,我们下面再来看一个_distance3版本的实现,加深对它的理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// distance版本3:返回值+SFINAE template<typename Iter> typename _enable_if<_is_same<typename std::iterator_traits<Iter>::iterator_category, std::random_access_iterator_tag>::value, size_t>::type _distance3(Iter s, Iter e) { std::cout << "_distance3 版本A" << std::endl; return e - s; } template<typename Iter> typename _enable_if<!_is_same<typename std::iterator_traits<Iter>::iterator_category, std::random_access_iterator_tag>::value, size_t>::type _distance3(Iter s, Iter e) { size_t d = 0; while (s != e) { ++s; ++d; } std::cout << "_distance3 版本B" << std::endl; return d; } |
这里只是把sfinae思路放到了_distance3的返回值中,这样编译器就可以执行到_enable_if来进行类型计算并发生我们期望的替换失败,这里巧妙在于_enable_if的<bool, size_t>的第2个模板参数是size_t也就是原本_distance3的返回值类型,当bool为false时则会令编译器继续尝试其他模板函数版本,也就是我们想要的sfinae机制,妙不妙?
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
