8591 字
43 分钟
C++ 模板笔记(二):变参模板与基础技巧
2022-12-14

第四章 变参模板#

编译期变参模板展开有四种方式:

  • 函数的递归调用
  • std::initializer_list
  • enable_if
  • 折叠表达式

4.1 函数的变参模板#

  • 基本定义
template <typename T, typename... Args> //注意这里的Args叫模板参数包
// 如果函数参数列表中一个参数的类型是一个模板参数包,
// 则此参数也是一个函数参数包
void func(const T& t, const Args&... rest); //这里的rest叫函数参数包
  • 一定要注意区分开在模板头声明的模板参数包和函数头的函数参数包的区别。

  • 注意,模板参数包只能接受同一个类型的参数。要么全是类型模板参数,要么全是非类型模板参数。要么全是模板类型模板参数。不能混用。这也是下面5.7匹配问题关于array的问题 ----- 12.3.4章节,12.2章节

  • C++17之前的写法:因为没有折叠表达式。这种属于函数递归调用

  • **当...出现在变量名字左侧的时候,表示声明一个参数包。一个参数包可以绑定零个或多个参数。当...出现在变量名字右侧的时候,表明它会被展开(unpack)为独立的参数。**出自这里

    • 包展开的场所查看这里
  • 包展开的模式。

    • 什么是模式?简要而言,模式就是看你省略号想要和什么东西组合在一起,也就是想要扩展什么。在包展开的场所中,以函数形参列表为例。我们可能看到两种样式的包展开:

      • do_something(func(args)...);
        //扩展为 do_something(func(var1), func(var2), func(var3));
      • do_something(func(args...));
        //扩展为 do_something(func(var1, var2, var3));
      • 格外注意第一种情况下,意味着do_something函数有三个参数。然而这个括号里面的逗号并不是逗号运算符,所以三个func()函数的调用的求值顺序并不被保证。

      • 以上例子来自

    • 我们看到了两种形式的包展开。所以所谓包展开的模式,也就是我们是想扩展的那个东西。一般来说,它会扩展它左侧的,和它贴在一起的那一个东西。例子:

    • void func1(int a, int b, int c){
      cout <<"func1" << endl;
      cout << a << b << c << endl;
      }
      int func2(int a){
      cout <<"func2" << endl;
      cout << a << endl;
      return a;
      }
      template<typename... Args>
      void tfunc(Args... args){
      func1(func2(args)...); //会被扩展为func1(func2(1), func2(3), func2(5));
      cout <<"--------------" << endl;
      func1(args...); //会被扩展为func1(1,3,5);
      cout <<"--------------" << endl;
      (func2(args), ...); //这是折叠表达式, C++17
      }
      int main(){
      tfunc(1,3,5);
      return 0;
      }
      /*
      func2
      5
      func2
      3
      func2
      1
      func1
      135
      --------------
      func1
      135
      --------------
      func2
      1
      func2
      3
      func2
      5
      */
    • 再次提示在这个func1(func2(args)...);包展开中,函数调用的求职顺序不保证,这不是逗号运算符。所以GCC中的顺序是5 3 1,但是clang的顺序是1 3 5

void print (){
//必须要有无参重载,否则会无限递归。也就是最后一次无参无法被调用。
//最后一次被解包后,参数包会为空。所以会调用无参函数。
}
template<typename T, typename... Args>
void print (T firstArg, Args... args)
{
cout << firstArg << endl; //print first argument
print(args...); // call print() for remaining arguments
}
int main()
{
print(1, 1000, "b23", 1.123, "HahaahaH", 42);
return 0;
}
  • C++17之后的写法:有了折叠表达式
    • 此处折叠表达式和逗号运算符语法会在下文补充。
template<typename T> //要有单参打印的重载。不然会无限调用。
void print(T t){
cout << t << endl;
}
template<typename... Args>
void print(Args... args){
(print(args), ...); //这里外部一定要加括号。注意语法。这里的,是逗号运算符。
}
int main()
{
print(1, 1000, "b23", 1.123, "HahaahaH", 42);
return 0;
}
  • 多种变长参数模板可以同时存在 尽管直观看起来会有二义性
  • 非变参模板比变参模板更特化。
template<typename... Args>
void ttss(Args... args){
cout << "varidic" << endl;
}
template<typename T1, typename T2, typename T3, typename T4>
void ttss(T1 a, T2 b, T3 c, T4 d){
cout << "4 args" << endl;
}
int main()
{
ttss("a", "v", "f", 5);
return 0;
}
//输出4 args

搭配std::initializer_list#

  • 函数变长参数模板搭配搭配std::initializer_list。
    • 这个函数会返回一个T类型的vector,元素是args
template<typename T, typename... Args>
vector<T> func(const Args&... args){
return {args...};
}
int main(){
auto vec = func<int>(1,2,3,4,5,6,7);
for(auto& i:vec){
cout << i << endl;
}
}
/*
输出:
1
2
3
4
5
6
7
*/
  • 如果目的不是初始化一个容器而是打印列表,可以化简为:
template <typename... Args>
void FormatPrint(const Args&... args)
{
std::initializer_list<int>{ (std::cout << "[" << args << "]", 0)...};
std::cout << std::endl;
}

我们知道逗号表达式会计算每一个表达式然后只保留最后一个表达式的值。(a, b) 这个表达式的值就是 b,那么上述代码中(std::cout << "[" << args << "]", 0)这个表达式的值就是0,但是前面的每一个表达式都会被计算。逗号表达式保证其中的内容从左往右执行,args参数包会被逐步展开,表达式前的(void)是为了防止变量未使用的警告,运行过后我们就得到了一个N个元素为0的初始化列表,内容也被格式化输出了。

int main()
{
FormatPrint(1, 2, 3, 4);
FormatPrint("good", 2, "hello", 4, 110);
return 0;
}
  • 当然了,把initializer_list换成任何一个接受initializer_list的容器都可以。比如vector也可以

4.1.2 变参和非变参模板的重载#

我们提到了多种变长参数模板可以同时存在 尽管直观看起来会有二义性。所以上面C++17前的写法可以写成这样子:

template<typename T>
void print (T arg){
cout << arg << endl;
}
template<typename T, typename... Args>
void print (T firstArg, Args... args)
{
print(firstArg); //注意这里。这里调用了无模板参数包的函数。单独打印这次解包出来的这一个变量。
print(args...); // call print() for remaining arguments
}
int main()
{
print(1, 1000, "b23", 1.123, "HahaahaH", 42);
return 0;
}
  • 简单来说,当两个函数模板的区别只在于尾部的参数包的时候,会优先选择没有尾部参数包的那一个函数模板。

4.1.3 sizeof运算符在变长模板中的特殊应用。#

沿用上面的例子,传递给 print() 的第一个参数之后,输出剩余两次参数的数量。对于模板参数包和函数参数包都可以使用 sizeof...。

template<typename T, typename... Args>
void print (T firstArg, Args... args)
{
print(firstArg);
cout << "remains"<<sizeof...(args) << endl; //函数参数包
cout << "remains"<<sizeof...(Args) << endl; //模板参数包
print(args...);
}

4.2 折叠表达式 (fold expression)#

折叠表达式的展开过程并不是递归。更多的是类似于一种替换,一种编译时展开替换。个人理解。

折叠表达式分为四种:

  • 左折:参数从左侧开始计算

    • 一元左折叠
    ( ... 运算符 形参包 )
    会被拓展为
    (((E1 运算符 E2) 运算符 ...) 运算符 EN)
    • 二元左折叠
    ( 初值 运算符 ... 运算符 形参包 )
    会被拓展为
    ((((初值 运算符 E1) 运算符 E2) 运算符 ...) 运算符 EN)
  • 右折:参数从右侧开始计算

    • 一元右折叠
    ( 形参包 运算符 ... )
    会被拓展为
    (E1 运算符 (... 运算符 (EN-1 运算符 EN)))
    • 二元右折叠
    ( 形参包 运算符 ... 运算符 初值 )
    会被拓展为
    (E1 运算符 (... 运算符 (EN−1 运算符 (EN 运算符 初值))))
  • 左折叠右折叠在有顺序要求的时候尤其重要。比如减法和除法。

    • 注意左右不是指的是单个顺序颠倒。而是每一组顺序颠倒。

    • 比如 ((a+b)+c) 变成右折并不是(a+(c+b))。而是(a+(b+c))

  • 将一元折叠用于长度为零的包展开时,只能使用下列运算符:

    • 逻辑与(&&)。空包的值是 true

    • 逻辑或(||)。空包的值是 false

    • 逗号运算符(,)。空包的值是 void()

    • 其他操作符为不合规。

  • 在二元折叠中,两个运算符必须相同

    • 不可以一加一减。
  • 语法示范:

//一元左折叠加法
template<typename... T>
auto foldSumLeft(T... s)
{
return (... + s); // ((s1+s2)+s3)...
}
//一元右折叠加法
template<typename... T>
auto foldSumRight(T... s)
{
return (s + ...); // (s1+...+(Sn-1 + sn)))...)
}
//一元左折叠减法
template<typename... T>
auto foldMinLeft(T... s)
{
return (... - s); // ((s1-s2)-s3)...
}
//一元右折叠减法
template<typename... T>
auto foldMinRight(T... s)
{
return (s - ...); // (s1-...-(Sn-1 - sn)))...)
}
int main(){
auto sumLeft = foldSumLeft(1,2,3,4,5,6,7); //((((((1+2)+3)+4)+5)+6)+7)
auto sumRight = foldSumRight(1,2,3,4,5,6,7);
cout << sumLeft << endl; //28
cout << sumRight << endl;//28
auto sumLeftmin = foldMinLeft(1,2,3); //((1-2)-3)
auto sumRightmin = foldMinRight(1,2,3);//(1-(2-3))
cout << sumLeft << endl; //-4
cout << sumRight << endl;//2
}
//二元左折
template <typename... T>
auto foldBinLeft(T... s)
{
//操作对象有两个,一个是888 初值,一个是ts形参包,
//初始值位于左边,所以为二元左折
return (888 + ... + s);
}
//二元右折
template <typename... T>
auto foldBinRight(T... s)
{
//操作对象有两个,一个是888 初值,一个是ts参包,
//初始值位于右边,所以为二元右折
return (s + ... + 888);
}
int main(){
auto binLeft = foldBinLeft(1,2,3); //894 (((888+1)+2)+3)
auto binRight = foldBinRight(1,2,3); //894 (1+(2+(3+888)))
cout << binLeft << endl;
cout << binRight << endl;
}
  • 如果用作 初值 或 形参包的表达式在顶层具有优先级低于转型的运算符,那么它必须加括号:
  • 此处仅用二元右折做个示范。注意此时两个操作符都是+。所以合法。
template <typename... T>
auto foldBinRight(T... s)
{
return (s + ... + (888*2)); //可以
return (s + ... + 888*2); //不可以
}

序列点和逗号运算符在折叠表达式中的特殊性质#

序列点的定义可以查一下文档。简单来说就是我们需要约束一个表达式的计算顺序。

我们上面提到了这个代码:

template<typename... Args>
void print(Args... args){
(print(args), ...); //这里外部一定要加括号。注意语法
(..., print(args));//??
}

我们发现第一行是右折叠。按理来说应该是逆序输出。但是为什么这里左折和右折效果一样?

这就是序列点和逗号运算符的特殊性。

  • C++有强制规定:内建逗号运算符 , 的第一个(左)参数的每个值计算和副作用都按顺序早于第二个(右)参数的每个值计算和副作用。
  • 也就是说,这里的左折叠和右折叠的优先级是低于逗号运算符的。所以说逗号运算符强制从左到右的运算顺序使得这里的左折和右折失效。产生了同等效果 。
  • 所以,这里在展开后,依旧会从左至右进行运算。
  • 再次重申:折叠表达式的展开过程并不是递归。
  • 所以它展开后可以被化简看作:
print(1), print(1000), print("b23"),.....;

此部分参考:

https://stackoverflow.com/questions/46056268/order-of-evaluation-for-fold-expressions

https://stackoverflow.com/questions/59590426/variadic-template-argument-forwarding-uses-comma-operator

https://stackoverflow.com/questions/53330713/fold-expression-with-comma-operator-and-variadic-template-parameter-pack

https://stackoverflow.com/questions/45603533/how-does-folding-over-comma-work

4.3 变参模板的使用#

之前关于常规模板参数的规则同样适用于变参模板参数。

  • 比如,如果参数是按值传递的,那么其参数会按照正常理解被拷贝,类型也会退化(decay)。
  • 如果是按引用传递的,那么参数会是实参的引用,并且类型不会退化

4.4 变参类模板和变参表达式#

4.4.1 在变参中使用表达式#

此处是针对4.2的一些扩展。也就是折叠表达式的一种应用。

我们可以将函数参数包参与到一些运算当中:

  • 这里,我们把传入的每一个参数的数值都增加一倍
    • 注意字面值是const char*类型。
template<typename T> //要有单参打印的重载。不然会无限调用。
void print(T t){
cout << t << endl;
}
template<typename... Args>
void print(Args... args){
(print(args), ...); //这里外部一定要加括号。注意语法。这里的,是逗号运算符。
}
template<typename... T>
void printDoubled (T const&... args)
{
print (args + args...);
}
int main(){
printDoubled(1, 2.3, string("abcde")); //输出 2 4.6 abcdeabcde。注意字面值是`const char*`类型。
//等于调用 print(1+1), print(2.3 + 2.3), print(string("abcde") + string("abcde"))
return 0;
}
  • 如果只是想每一个都加1,那么后面的参数包位置则必须字面值和数字分开。
template<typename... T>
void printDoubled (T const&... args)
{
print(1 + args...); //要么这么写
print(args + 1 ...); //要么这么写。但是1和...必须分开。
print((args + 1)...); //要么这么写。多加个括号。
}
  • 编译阶段的表达式同样可以像上面那样包含模板参数包。比如下面这个例子可以用来判断所有参数包中参数的类型是否相同
    • 注意这里是函数模板的例子。函数模板可以自动推导类型。
template<typename T1, typename... TN>
constexpr bool isSameType (T1, TN...)
{
return (std::is_same<T1,TN>::value && ...); // since C++17
}
int main(){
cout << isSameType(1,2,3,4) << endl; //输出1 true
//这一行会被扩展为 std::is_same<int,int>::value && std::is_same<int,int>::value && std::is_same<int,int>::value
cout << isSameType(1,2,"123",4) << endl; //输出0 false
//这一行会被扩展为 std::is_same<int,int>::value && std::is_same<int,const char*>::value && std::is_same<const char*,int>::value
cout << isSameType("abc", "abcde") << endl; //输出1 true 特别注意这里
return 0;
}
  • 注意最后一个调用。这里因为是按值传递,所以发生了类型退化,统一被推导为const char*。
  • 否则类型将依次被推断为:const char [3] 和 const char[5];
    • 所以如果isSameType写成这个样子就不会退化,导致输出false
    • constexpr bool isSameType (T1&, TN&...)
      • 这里参见函数模板参数推导规则。这里我们看到传入的是数组,然后模板函数的入参部分(不是模板参数部分)是引用了,所以这时候传入的参数不会退化。也就是里面的T1和TN维持住了非退化类型。

4.4.2 在变参中使用下标 (variadic indices)#

这里看起来可能比较晦涩,但是是一个比较实用的功能。

  • 假设我们有两个参数。第一个参数是一个容器,第二个参数是一个下标。我们想访问这个容器的这个下标的元素应该怎么做?
template<typename Container, typename Index>
void getItem(Container& container, Index idx){
cout << container[idx] << endl;
}
int main(){
//printDoubled(1, 2.3); //输出 2 4.6
vector<string> my_vec{"abc", "def", "ghi", "123", "456"};
getItem(my_vec, 3); //输出 123
return 0;
}
  • 好的。那么我们如果想要把这个功能融入到变参里面呢?比如我想要拿出多个元素,怎么做呢?
    • 注意这里我们参数包为下标,而不是容器。
    • 非常方便。
template<typename T> //要有单参打印的重载。不然会无限调用。
void print(T t){
cout << t << endl;
}
template<typename... Args>
void print(Args... args){
(print(args), ...); //这里外部一定要加括号。注意语法。这里的,是逗号运算符。
}
//--------------------------上面是之前我们一直在用的print模板,这里也放一个方便查看---------------------
template<typename Container, typename... Index> //模板参数包
void getMultipleItem(Container& container, Index... idx){//函数参数包
print(container[idx]...); //参数包
}
int main(){
vector<string> my_vec{"abc", "def", "ghi", "123", "456"};
getMultipleItem(my_vec, 1,2,3,4);
//输出def ghi 123 456
return 0;
}
  • getMultipleItem(my_vec, 1,2,3,4); 相当于调用了:
    • print(my_vec[1], my_vec[2], my_vec[3], my_vec[4]);

同时我们也可以将非类型模板参数声明成参数包:#

template<int... Idx, typename Container> //细节1
void getMultipleItem(Container& container){ //细节2
print(container[Idx]...);
}
int main(){
vector<string> my_vec{"abc", "def", "ghi", "123", "456"};
getMultipleItem<1,2,3,4>(my_vec); //细节3
//输出def ghi 123 456
return 0;
}
  • 首先,非类型模板参数不是类型,是变量。所以无需传入函数参数。只需在模板参数中传入(细节1,2)
  • 细节3,这里既然是模板参数而非函数参数,所以这里要用<>传入模板参数

4.4.3 在类模板中使用可变参数(变参类模板)#

等25和26章

4.4.4 变参推断指引#

看书

4.4.5 变参基类#

等26章

第五章 基础技巧#

5.1 typename关键字#

看STL2

5.2 零初始化#

此处同时参考笔记 聚合初始化

  • 模板中依旧遵循和非模板相同的初始化规则,比如:
template <typename T>
void func(){
T x; //如果T是内置类型,则模板实例化后,x为脏数据,也就是未确定值。
}
  • 如果想要强制其初始化为0,则应该使用值初始化。关于值初始化依旧参考聚合初始化笔记。
template <typename T>
void func(){
T x{}; //如果T是内置类型,则模板实例化后,x为0值。
}

一些之前的知识回顾#

  • 在 C++11 之前,确保一个对象得到显示初始化的方式是
T x = T();
  • [这一条可以参考杂记的拷贝初始化和杂记2的explicit] 由于这是一种拷贝初始化,所以在 C++17 之前,只有在与拷贝初始化对应的构造函数没有被声明为 explicit 的时候,这一方式才有效(目前也依然被支持)
  • [强制拷贝省略技术可以参考杂记3的复制省略技术] 从 C++17 开始,由于强制拷贝省略(mandatory copy elision)的使用,这一限制被解除,因此在 C++17 之后以上两种方式都有效。不过对于用花括号初始化的情况,如果没有可用的默认构造函数,它还可以使用列表初始化构造函数(initializer-listconstructor)

继续话题#

为确保类模板中类型被参数化了的成员得到适当的初始化,可以定义一个默认的构造函数并在其中对相应成员做初始化:

template<typename T>
class MyClass {
private:
T x;
public:
MyClass() : x{} {} //确保当T为内置类型时,x值为0。注意第一个花括号为值初始化。第二个花括号是构造函数的函数体。
};
  • c++11之前的语法:
MyClass() : x() {}
  • 从C++11开始,针对非静态成员和非默认参数部分,则也可以用这种方法。
template<typename T>
class MyClass {
private:
T x {}; // 使用值初始化,确保当T为内置类型时,x值为0。
};
  • 如果针对默认参数这样使用,则会报错。
template<typename T>
void foo(T p{}){
//错误
}
  • 这样可以:
template<typename T>
void foo(T p = T{}){
//正确。但如果C++11之前的话必须把T{}换成T()
}

5.3 使用this#

  • 这里应参考 深度探索c++对象模型笔记中的7.1
  • 简而言之,当在模板类中使用定义于基类中的、依赖于模板参数的成员时,应当用 this->或者如 Base<T>::这样显式指定作用域来使用它

5.4 正确处理传入模板的裸数组或字符串常量#

  • 当向模板传递裸数组或者字符串常量时,需要格外注意以下内容:

  • 如果参数是按引用传递的,那么参数类型不会退化(decay)。

    • 也就是说当传递hello作为参数时,模板类型会被推断为 const char[6]。这样当向模板传递长度不同的裸数组或者字符串常量时就可能遇到问题,因为它们对应的模板类型不一样。
  • 只有当按值传递参数时,模板类型才会退化(decay)

    • 这样字符串常量会被推断为const char*。

关于具体选用按照值传递还是引用传递,可以看第七章。

不过我们可以单独处理一下裸数组或者是字符串常量

template<typename T, int N, int M>
bool func (T(&a)[N], T(&b)[M]){
cout << N << endl; //输出3
cout << M << endl; //输出7
for (int i = 0; i<N && i<M; ++i){
if (a[i]<b[i]){
return true;
}
if (b[i]<a[i]){
return false;
}
}
return N < M;
}
int main(){
int x[] = {1,2,3};
int y[] = {1,2,3,4,5,6,7};
func(x,y);
return 0;
}

在上面的代码中,func中的T会被实例化为int,N会被实例化为3,M会被实例化为7。

同样适用于字符串常量。如果我们调用

func("abc", "abcde");

则func中的T会被实例化为const char,N会被实例化为4,M会被实例化为6。这里多了1是因为有字符串结束符。

5.5 成员模板#

我们知道,写在尖括号<>内部的是模板参数,也就是类型信息。所以stack<int> 和 stack<float>理论上不可以相互赋值。因为没有合适的转换方式。但是我们可以重载 类内的operator=来实现这个不同类型间的相互赋值。比如stack<float>赋值给stack<int>。

具体实现还是看书比较好。

5.5.1 成员模板的特化#

我们也可以给成员函数添加模板。同时成员函数模板也可以被全特化

class getInt{
public:
float val;
getInt(float a): val(a){};
template<typename T = int> //成员函数模板。此处模拟一个返回int
T get() const{
return val;
}
};
template<> //全特化,此处模拟返回string。这里有细节
inline string getInt::get<string>() const{ //注意这里的inline
return to_string(val);
}
int main(){
getInt myobj(12.345);
auto t = myobj.get();
cout << t << endl; //12
auto t1 = myobj.get<string>(); //12.345000
cout << t1 << endl;
return 0;
}
  • 第一个细节:c++不允许成员函数模板在类内全特化(显式特化)。必须写在类外。
  • 第二个细节:如果分离编译,则此处全特化版本函数必须inline,否则会重定义。

5.5.2 特殊成员函数的模板 和 泛型lambda#

  • 如果能够通过特殊成员函数 拷贝或者 移动对象,那么相应的特殊成员函数(拷贝构造函数以及 移动 构造函数)也将可以被模板化。

  • 和前面定义的赋值运算符类似,构造函数也可以是模板。但是需要注意的是,构造函数模板或者赋值运算符模板不会取代预定义的构造函数(此处尤指拷贝构造和移动构造)和赋值运算符(此处尤指拷贝赋值和移动赋值)。成员函数模板不会被算作用来 拷贝或移动 对象的特殊成员函数。在上面的例子中,如果在相同类型的 stack 之间相互赋值,调用的依然是默认赋值运算符。

    • 这句话有毛病,省略了几个词。经过我的多方咨询,这句话的意思是:构造函数模板就算发现参数推导匹配,也不会为我们生成拷贝构造/拷贝赋值/移动构造和移动赋值。这几个函数必须是预定义的而不能是从模板实例化的。也就是说,就算模板实例化的函数长得表面和那四个函数一样,实例化出的函数也不会被认为是特殊成员函数。----同时参考effective modern C++ 条款26

QQ截图20230114134638

QQ截图20230114134644

  • 这种行为既有好处也有坏处:
    • 某些情况下,对于某些调用,构造函数模板或者赋值运算符模板可能比预定义的拷贝或移动 构造函数或者赋值运算符更匹配,虽然这些特殊成员函数模板可能原本只打算用于在不同类型的某一个类之间做初始化。详情请参见 6.2 节。
    • 想要对拷贝或移动构造函数进行模板化并不是一件容易的事情,比如该如何限制其存在的场景。详情请参见 6.4 节。

泛型lambda [c++14]:#

auto lambda = [](auto x, auto y){
return x+y;
}

在这里,我们创建了一个泛型lambda。这个lambda可以接受任意的x和y,只要x和y可以相加,这段代码就没问题。

它的匿名类可能会像这样:

class SomeCompilerSpecificName {
public:
SomeCompilerSpecificName(); // constructor only callable bycompiler
template<typename T1, typename T2> //函数模板
auto operator() (T1 x, T2 y) const {
return x + y;
}
};

这个部分在std::visit中有用到。节选自这里

int main()
{
std::vector<var_t> vec = {10, 15l, 1.5, "hello"};
for (auto&& v: vec)
{
// 1. void 探访器,仅为它的副作用而调用
std::visit([](auto&& arg){ std::cout << arg; }, v);
// 2. 返回值的探访器,演示返回另一变体的常见惯用法
var_t w = std::visit([](auto&& arg) -> var_t { return arg + arg; }, v);
// 3. 类型匹配探访器:以不同方式处理每个类型的 lambda
std::cout << "。翻倍后,变体持有";
// 这一部分就是泛型lambda。
std::visit([](auto&& arg)
{
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>)
std::cout << "值为 " << arg << " 的 int\n";
else if constexpr (std::is_same_v<T, long>)
std::cout << "值为 " << arg << " 的 long\n";
else if constexpr (std::is_same_v<T, double>)
std::cout << "值为 " << arg << " 的 double\n";
else if constexpr (std::is_same_v<T, std::string>)
std::cout << "值为 " << std::quoted(arg) << " 的 std::string\n";
else
static_assert(false, "探访器无法穷尽类型!");
}, w);
}
for (auto&& v: vec)
{
// 4. 另一种类型匹配探访器:有三个重载的 operator() 的类
// 注:此情况下 '(auto arg)' 模板 operator() 将绑定到 'int' 与 'long',
// 但它不存在时 '(double arg)' operator() *也会* 绑定到 'int' 与 'long',
// 因为两者均可隐式转换到 double。使用此形式时应留心以正确处理隐式转换。
// 这一部分可以查看 笔记尾的《继承自可变参数包》一节
std::visit(overloaded{
[](auto arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
}, v);
}
}

5.5.3 template 关键字 (如 .template , ::template, ->template)#

template关键字是用来消除待决名的歧义的. 观察下面的代码:

template<class T>
int f(T& x) {
return x.template convert<3>(pi);
}

如果没有template, 则

return x.convert<3>(pi);

可能被理解为

return ((x.convert) < 3) > (pi);
  • 所以使用template来显式说明convert不是一个数据成员, 而是一个模板函数. 下面把标准照抄一遍.

使用 template 的规则

当成员模板特化的名字出现在一个后缀表达式中的.或->之后, 或者出现一个限定标识中的嵌套的名字修饰符之后(就是::), 并且后缀表达式或限定标识显示依赖于一个模板参数时, 成员模板名字必须加template关键字作为前缀, 否则该名字就被假定为一个非模板的名字.

如果后缀表达式或者限定标识不是出现在一个模板的作用域时, 成员模板的名字就不应该加上template关键字作为前缀.

必须使用template的场合

在通过“.”,“->”,“::”限定的依赖名访问成员模板之前, template关键字必不可少.

template<class T>
void f(T& x, T& y) {
int n = x.template convert<int>();
int m = y->template convert<int>();
}
template<class T> struct other;
template<class T>
struct dirived : other <T>::template base<int> {};

禁止使用template的场合

禁止用在模板之外的任何地方, 包括显式(完全)模板特化. 禁止用在using声明中.

5.6 变量模板#

5.6.1 普通变量模板#

在C++14之后,我们可以对变量使用模板。

template<typename T>
T my_val = 20.1234;
int main(){
cout << my_val<int> << endl; //20
cout << my_val<double> << endl; //20.1234
}
  • 注意,和其它几种模板类似,这个定义最好不要出现在函数内部或者块作用域内部。不可在main函数内。
  • 变量模板可以有默认参数。
template<typename T = double> //默认参数
T my_val = 20.1234;
int main(){
cout << my_val<int> << endl; //20
cout << my_val<> << endl; //20.1234
}
  • 在使用变量模板的时候,必须显式指明它的类型。如果直接使用变量会报错。
cout << my_val << endl; //错误
  • 同样可以用非类型参数对变量模板进行参数化,也可以将非类型参数用于参数器的初始化。

5.6.2 成员变量模板#

虽然不知道有啥用,但是还是整理一下。

假如我们有如下类模板:

template<typename T>
class myobj{
public:
static const int val = 1000; //static 和 const一起修饰变量可以在类内赋初值
};

如果我们不用成员变量模板,类外想访问的话比较麻烦:

int out_val = myobj<int>::val;
int main(){
cout << out_val << endl;
return 0;
}

如果我们采用成员变量模板,则可以这样使用:

template <typename T>
int out_val_T = myobj<T>::val; //成员变量模板
int out_val1 = out_val_T<int>; //直接使用
int main(){
cout << out_val1 << endl;
return 0;
}

5.7 模板模板参数#

我们有过非类型模板参数,意思就是模板参数并不指定类型。

所以这里的模板模板参数的意思就是让模板的参数也是模板。

模板的模板参数的经典应用是在某些自定义指定储存容器类型的时候不指定元素类型。

比如,在我们不用模板模板参数的时候,指定stack可能需要这么写

Stack<int, std::vector<int>> vStack; //integer Stack that uses a vector

如果使用模板模板参数,我们就可以这么写:

Stack<int, std::vector> vStack; //intege stack that uses a vector
  • 函数模板和变量模板没有模板模板参数、

  • 最基本的举例

template<template<typename T> class container, typename T1, typename T2> // 最基本的举例
class test{
container<T1> a1;
container<T2> a2;
};
  • 茴字的三种写法, 都可以
template <typename T, template <typename> class Container>
template <typename T, template <class> class Container>
template <typename T, template <typename> typename Container>
  • 更好的理解的写法:
template <typename T, template <typename T1> typename Container>
  • 在上面的例子里面,我们用不到模板模板参数里面的占位符,所以也可以忽略不写。

模板模板参数期待的是模板名,而不是具体类型#

一定要注意模板模板参数到底期待的是什么。

template <typename T1, typename T2>
struct S{
};
template <template <typename, typename> typename T1>
struct A{
T1<int, double> obj; //可以在类内通过模板名T1这样实例化。
};
int main(){
A<S> obj1; //模板模板参数期待的是一个模板名。
A<S<int, double> obj2; //错误,不能这样指定,这是具体类型了。
}

在模板声明体内,此形参(我们的例子是T1)的名字是一个模板名(且需要实参以实例化)。

模板模板参数的语义#

我们反复强调,模板模板参数的意义是让模板参数本身也成为模板,同时模板模板参数期待的是模板名而不是类型名。

所以:

template <typename T1, typename T2>
struct S{
};
template <typename T, template <typename, typename> typename T1 = S>
struct A{ //这里模板模板参数的默认值必须是一个模板名,不可以是S<T,int>这样的具体类型名
T1<T, double> obj; //可以在类内通过模板名T1这样实例化。
};
int main(){
A<int, S> obj1; //模板模板参数期待的是一个模板名。
}

如果换成非模板模板参数的写法,则应该是这个样子:

template <typename T1, typename T2>
struct S{
};
template <typename T, typename T1 = S<T, double>>
struct A{ //非模板模板参数的写法
T1 obj;
};
int main(){
A<int> obj1;
}

模板模板参数的匹配问题#

  • 我们的第一个例子使用模板模板参数 [C++14]:
template <typename T, template <typename Elem, typename = std::allocator<Elem>> typename Container = std::vector>
class Stack {
public:
Container<T> elems; // container<T>会默认调用对应的容器的默认分配器
};
int main(){
Stack<int, deque> sss; //一个stack,实现基于deque类,储存int类型数据
Stack<int> ssss; //一个stack,使用默认值实现基于vector类,储存int类型数据
}
  • 上面的这段代码,是基于C++14的。我们有一个默认值。但是为什么我们显式指明了分配器?因为在C++17之前,模板模板参数必须和实际参数的模板参数匹配。因为vector和deque有两个模板参数,第一个是元素类型,第二个是分配器。虽然分配器有默认参数,但是默认参数也要被匹配。所以如果不写分配器就会出现缺少参数的情况

所以如果到了C++17,我们就可以这么写。去掉分配器:

template <typename T, template <typename> typename Container = std::vector>

但是这个时候会有个问题,也就是此时这样做会让stack模板强制要求容器模板支持第二个参数。但是比如std::Array就不支持第二个参数。因为它的第二个参数是非类型模板参数。所以这样写就不行。

Stack<int, array> sss;
  • 所以如果在C++17前,我们不想指定分配器怎么办?方法一,使用别名模板。这时候别名模板的分配器会支持默认值。
    • 如果我们想使用array,只需要别名模板就可以。注意array的第二个参数必须给。因为没有默认值。
template <typename T>
using deq = deque<T>; //要不然用别名模板也算是显式指定容器元素类型和分配器。
template <typename T>
using arr = array<T, 10>; //注意array的第二个参数必须给。因为没有默认值。
template <typename T, template <typename> typename Container>
class Stack {
public:
Container<T> elems; // container<T>会默认调用对应的容器的默认分配器
};
int main(){
Stack<int, deq> sss; //一个stack,实现基于deque类,储存int类型数据
Stack<int, arr> ssss; //一个stack,实现基于array类,储存int类型数据
}
  • 方法二,使用可变参数模板在一定程度上简化写法。但是这里array依旧不可以。因为模板参数包只能匹配相同种类的模板参数。但是array的第一个是类型模板参数,而第二个是非类型模板参数。所以想要用array依旧需要用别名模板。 ---12.3.4章节
template <typename T, template <typename ...> class Container> //使用可变参数模板简化后的方式 注意array不可以 array的第二个参数是非类型模板参数
class Stack {
public:
Container<T> elems; // container<T>会默认调用对应的容器的默认分配器
};
int main(){
Stack<int,deque> ss; //一个stack,实现基于deque类,储存int类型数据
}

最后,我们梳理一下那一大堆模板头的含义:

↓container类的默认参数的类型。因为没用到可以忽略 ↓ container类的默认参数。可以不写
template <typename T, template <typename U, typename Alloc = allocator<U>> class Container = deque>
↑这个T意思是储存T类型数据 ↑这个Alloc是contain类的默认方法的U类型的分配器。因为没用到可以忽略
//上面这样做会让stack模板强制要求容器模板支持第二个参数
template <typename T, template <typename ...> class Container> //使用可变参数模板简化后的方式
template <typename T, template <typename, typename> class Container> //不能这么写,下面container<T>会少一个参数,但是你上面必须写俩必须写分配.

模板模板参数还可以做什么?---继承自类模板#

当我们需要继承自一个类模板的时候,可以使用模板模板参数

template <typename T1, typename T2>
struct S{
void func(){
std::puts(__PRETTY_FUNCTION__);
cout <<"called" << endl;
}
};
template <typename T, typename T1, template<typename, typename > typename T2> //T2是模板模板参数,接受一个模板名
struct A: T2<T, T1>{//继承自T2类,T和T1是实例化T2所必需的模板参数
};
int main(){
A<int, double, S> obj;
obj.func();
}
C++ 模板笔记(二):变参模板与基础技巧
https://sterben-01.github.io/posts/模板-2/
作者
Sterben_01
发布于
2022-12-14
许可协议
CC BY-NC-SA 4.0