模板第一个最常用的应用是泛型编程,泛型编程主要关注通用算法的设计、实现和使用。
这里“通用”的含义是该算法能支持多种数据类型,只要类型符合算法对参数的要求即可。
模板提供了以下功能:
template
Value sum(Seq s, Value v) {
for(const auto& x : s)
v+=x;
return v;
}
这个sum函数需要保证
这种需求叫作概念。
满足作为序列要求的大致有:标准库vector、list、map
满足作为算术类型的大致有:int、double、Matrix(所有合理定义的矩阵都支持算术运算)。
从以下两个维度看,sum属于通用算法:
- 数据结构的类型(序列存储方式)维度;
- 数据元素的类型维度。
大多数模板参数必须符合特定需求才能被正常编译和运行。也就是说,绝大多数模板都应当是受限模板。
类型名称指示符typename是限定程度最低的,他仅仅要求该参数是一个类型。
template
Num sum(Seq s, Num v){
for(const auto& x : s)
v+=x;
return v;
}
以下这些巴拉巴拉一大堆也不知道讲的什么,做个标记,以后再看
先看看GPT的解释:
强调了使用概念来对模板参数进行约束的重要性,以提高代码的清晰度和安全性。通过使用概念,可以在编译期间发现一些错误,而不是等到运行时才暴露问题。
在C++中,
requires
子句是用于指定模板参数的一组要求(constraints)的关键字。它用于在模板定义中对模板参数进行约束,以确保只有符合指定条件的类型或值才能被接受。在模板中,
requires
子句通常用于requires
关键字之后,用于指定一组布尔表达式,这些表达式描述了模板参数必须满足的条件。如果这些条件不满足,编译器将拒绝对该模板的实例化,并在编译时生成错误消息。例如,在上面提到的代码中,
requires Arithmetic
表达了对于类型, Num> range_value_t
和Num
,必须满足Arithmetic
概念。这样的约束有助于确保在模板函数中对这些类型进行算术运算时是安全和合法的。
requires
子句的使用使得模板的错误能够更早地在编译期间被发现,提高了代码的可读性和安全性。在概念引入之前,开发者通常通过模板的SFINAE(Substitution Failure Is Not An Error)机制来实现类似的效果,但概念提供了更为直观和清晰的方式来表达对模板参数的要求。
但是,sum接口的技术规格不太完整:应该允许将整个Sequence的元素累加到Number。
template
requires Arithmetic,Num>
Num sum(Seq s, Num s);
vector
或vector
的sum()这样的操作。同时也可以正常支持vector
与vector>
这样的参数。requires Arithmetic
被称作requirements子句。其中记法template
就是比requires Sequence
更简单的写法。
复杂点则等价于:
template
requires Arithmetic && Number && Arithmetic,Num>
Num sum(Seq s, Num s);
同时,写成如下简写形式也具有等价的效果:
template<> Num>
Num sum(Seq s, Num n);
一旦我们正确地指定了模板地接口,就可以根据它们地属性进行重载,如同函数一样。
例如:标准库advance()函数向前移动迭代器,简化版本如下:
template
void advance(Iter p, int n) { // 将p向前移动n个元素
while(n--)
++p; // 前向迭代器拥有 ++ 操作符,但没有+或者+=操作符
}
template
void advance(Iter p, int n) { // 将p向前移动n个元素
p += n; // 随机访问迭代器拥有 += 操作符
}
编译器会选择满足最严格参数需求的版本。list只提供了向前迭代器,vector提供了随机访问迭代器。
因此:
void user(vector::iterator vip, list::iterator lsp)
{
advance(vip,10); // 使用快速版本的advance()
advance(lsp,10); // 使用慢速版本的advance()
}
如同其他的重载,这是编译时机制,没有任何开销;
如果编译器无法找到最佳选择,会报二义性错误。
考虑具有一个参数并且提供多个版本的模板函数:
选择某个特定版本的模板,必须满足这些条件:
C++直接支持的泛型编程形式围绕着这样的思想:从具体、高效的算法中抽象出来,从而获得可以与不同数据表示相结合的泛型算法,以生成各种有用的软件。
表示基本操作和数据结构的抽象被称为概念。
…较为复杂先不管,以后有能力再看
定义模板时可以令其接受任意数量、任意类型的实参,这样的模板被称为可变参数模板。
假设我们需要实现一个简单的函数,输出任意可以被 << 操作符输出的数据:
void user() {
print("first: ", 1, 2.2, "hello\n"s); // 输出first: 1 2.2 hello
printf("\nsecond: ", 0.2, 'c', "yuck!"s, 0, 1, 2, '\n'); // 输出second: 0.2 c yuck! 0 1 2
}
传统方法是:实现一个可变参数模板,将第一个参数剥离出来,然后用递归调用的办法处理所有剩下的参数:
template
concept Printable = requires(T t) { std::cout << t; } // 只有一个操作
void print()
{
// 处理无参数的情况:什么都不做
}
template
void print()
{
cout << head << ' '; // 首先对head进行操作
print(tail...); // 然后操作tail
}
每次调用print()都把参数分成头元素以及其他(尾)元素。对头元素调用了打印命令,然后对其他元素调用print()。最终,tail变为空,所以我们一定需要一个无参数的版本来处理空参数的情况。如果不需要处理无参数的情况,可以通过编译时if来消除这种情况。
template
void print(T head, Tail... tail)
{
cout << head << ' ';
if constexpr(sizeof...(tail) > 0)
print(tail...);
}
这里使用编译时if而不是运行时if,可以避免生成对空参数print()函数的调用。这也就无须定义空参数版本的print()。
可变参数模板的强大之处在于,它们可以接受任意参数。缺点包括:
template
int sum(T... v)
{
return (v + ... + 0); // 将v中所有元素与0累和
}
这里,(v + … + 0)表示把v中的所有元素加起来,从0开始。首先做加法的元素是最右边的那个(也就是索引最大的那个):(v[0]+(v[1]+(v[2]+(v[3]+(v[4]+0))))。从右边开始的叫作右折叠。
这个sum()函数可以接受任意数量、任意类型的参数:
int x = sum(1,2,3,4,5); // x变成15
int y = sum('a', 2.4, x); // y变成114(2.4被取整,‘a'的值是97
反之,左折叠:
template
int sum(T... v)
{
return (0 + ... + v); // 将v中所有元素与0累和
}
(((((0+v[0])+v[1])+v[2])+v[3])+v[4])
除此之外,折叠表达式不仅限于算术操作。
template
void print(T&&... args)
{
(std::cout << ... << args) << '\n'; // 打印输出所有参数
}
// (((((std::cout << "Hello!"s) << ' ') << "World ") << 2017) << '\n');
print("Hello!"s,' ',"World ",2017);
出现2017,是因为fold()的特性实在C++2017标准中被添加的。
使用可变参数模板时,保证参数在通过接口传递的过程中完全不变,有时非常有用。
。。。