7.2 Concepts(C++20)
请参考一下上一章中的sum()函数:
template<typename Seq, typename Num> Num sum(Seq s, Num v) { for(auto x : s) v += x; return v; }
该函数能够被任何支持begin()与end()的数据结构所调用。例如标准库中的vector,list与map。此外该类型的数据结构必须能够进行加法运算。例如int,doubble,Matrix。因此我们可以说sum()算法在两个方面是generic的:用于存储元素的数据结构的类型以及元素的类型。
所以,sum()的第一个template参数需要是某种sequence序列,而第二个template参数需要是某种数字。我们将这种要求称为concepts。
ISO C++还未支持concepts,但是ISO Technical Specification已经准许这一设定。
7.2.1 Use of Concepts
大多数template参数需要符合编译template的要求并且保证生成的代码能够正常运作。也就是说,大多数template必须是受限制的template。typename可以当作是限制最少的template参数,其只需要参数是一种类型即可。通常我们能够做得更好,因此对函数sum()进行了一定的改进。
template<Sequence Seq, Number Num> Num sum(Seq s, Num v) { for(auto x : s) v += x; return v; }
这使得代码更为清晰。当我们定义了Sequence与Number这两个concept之后,编译器只要通过接口就能拒绝错误的调用,并不需要接入函数的实现。不过上述sum()接口并不完整,我们还能够将Sequnce的元素加入到Number中。
template<Sequence Seq, Number Num> requires Arithmetic<Value_type<Seq>, Num> Num sum(Seq s, Num n);
上述代码中的Value_type表示Sequence中的元素的类型。Arithmetic也是一种concept,其要求传入的两个参数能够进行数学运算。通过这一限定,我们将以代码的形式展示我们的意图而不是注释。此外,错误的template会在编译时发生报错。还需要补充一点的是,上述代码形式是require的一种简写。
// 完整形式 template<typename Seq, typename Num> requires Sequence<Seq> && Number<Num> && Arithmetic<Value_type<Seq>, Num> Num sum(Seq s, Num n); // 组合形式 template<Sequence Seq, Arithmetic<Value_type<Seq>>Num> Num sum(Seq, Num n);
7.2.2 Concept-based Overloading
当我们为接口设定了template,之后就能基于template的属性进行重载,其类似于函数的重载。
template<Forward_iterator Iter> void advance(Iter p, int n) { for(--n) ++p; } template<Random_access_iterator Iter, int n> void advance(Iter p, int n) { p+=n; } void user(vector<int>::iterator vip, list<string>::iterator lsp) { advance(vip, 10); // 调用快速advance advance(lsp, 10); // 调用慢速advance }
上述代码中,编译器将会选择符合template参数要求的函数。显然,标准库中的list只提供了向前迭代器,而vector则提供了随机读取的迭代器。这一技术类似于函数的重载,其由编译器处理,因此不会产生运行时的开销。如果编译器无法找到匹配的选择,那么会发生报错。
7.2.3 Valid Code
template参数是否符合某一标准,其实就是该类型的template参数是否能运用于某些表达式。我们可以使用requires来检测某一表达式是否有效。
template<Forward_iterator Iter, int n> requires requires(Iter p, int i) { p[i]; p+i; } void advance(Iter p, int n) { p+=n; }
代码中连续出现的两个requires,第一个表示requirements法则的开始,第二个表示requires表达式。如果requires的表达式有效,那么其为true否则为false。
7.2.4 Definition of Concepts
我们可以在标准库中发现非常游泳的concepts,例如Sequence与Arithmetic。不过我们也可以自己定义见得concepts。concept是由编译器来检测某一类型能否被使用。
template<typename T, typename T2=T> concept Equality_comparable = requires (T a, T2 b) { { a == b } -> bool; { a != b } -> bool; };
我们可以使用Equality_comparable来保证两种类型能够使用相同或者不相同进行比较。其中的typename T2=T类似于参数中的默认值,其表示,如果我们不限制第二个template参数,那么它就是T。我们也可以使concept更为复杂。
template<typename S> concept Sequence = requires(S a) { typename Value_type<S>; // S必须拥有值类型 typename Iterator_type<S>; // S必须拥有迭代器类型 { begin(a) } -> Iterator_type<S>; // begin(a)必须返回一个迭代器 { end(a) } -> Iterator_type<S>; // end(a)必须返回一个迭代器 requires Same_type<Value_type<S>, Value_type<Iterator_type<S>>>; requires Input_iterator<Iterator_type<S>>; };
7.4 Variadic Templates
我们可以通过template定义一个有任意数量,任意类型参数的函数。这一种template就是所谓的variadic template。
void user() { print("first: ", 1, 2.2, "hello\n"s); // first: 1 2.2 hello print("\nsecond: ", 0.2, 'c', "yuck!"s, 0, 1, 2, '\n'); // second: 0.2 c yuck! 0 1 2 } void print() { // 如果没有参数,那么什么都不做 } template<typename T, typename ... Tail> void print(T head, Tail ... tail) { cout << head << ' '; print(tail...); }
一般来说,如果要实现一个variadic template,那么我们需要将第一个参数与之后的参数进行区分,之后不断通过variadic template来调用tail参数。上述代码中的typename …意味着Tail是一个sequnce类型。而Tail…意味着tail就是一组有Tail中的元素所组成的sequence。通过…声明的参数被称为一个parameter pack。
函数print()会将会吧参数分为head(第一个元素)与tai(其余元素)。head将被print之后再调用tail的print。最后tail将会变空,此时我们就需要没有参数的print()版本进行处理。如果我们不想要无参数版本的print,可以使用编译器版本的if。
template<typename T, typename ... Tail> void print(T head, Tail... tail) { cout << head << ''; if constexpr(sizeof...(tail)> 0) print(tail...); }
7.4.1 Fold Expressions
为了简化variadic template的实现,c++17提供了下列选择。
template<Number... T> int sunm(T... v) { return (v + ... + 0); // 累加v中的所有元素,并以0作为起始 }
函数sum()能够累加任意类型,任意数量的参数。函数中的(v+…+0)意味着v中所有的元素将会与初始值0开始累加。第一个相加的元素是“最右侧”的元素(也就是索引值最大的元素):(v[0]+(v[1]+(v[2]+(v[3]+(v[4]+0)))))。而最右侧的0被称为right fold。同样,我们也可以使用一个left fold。
template<typename... T> int sum2(T... v) { return (0 + ... + v); }
Fold是非常有用的抽象工具。在c++中,fold表达式只能用于简化variadic template的实现。fold并不一定要用来处理数字上的运算。
template<typename ...T> void print(T&&... args) { (std::cout << ... << args) << '\n'; // 打印所有的参数 } print("Hello!"s, ' ', "World ", 2017);
template<typename Res, typename... Ts> vector<Res> to_vector(Ts&&... ts) { vector<Res> res; (res.push_back(ts) ...); return res; } auto x = to_vector<double>(1,2,4.5,'a');
7.6 Advice
[01] template提供了一种编译器的泛型编程。