【C++泛型学习笔记】友元、可变参模板

学习参考书籍:王健伟《C++新经典:模板与泛型编程》

类模板中的友元

友元,即若A为B的友元,那么A可以访问B中的所有成员(任何修饰符修饰)。

友元类

1.类模板的实例成为友元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

template<typename U> class B;
template<typename T>
class A
{
friend class B<long>;
private:
int data;
};

template<typename U>
class B
{
public:
void callBAF()
{
A<int> atmpobj;
atmpobj.data = 5;
cout << atmpobj.data << endl;
}
};

int main()
{
B<long> bobj;
bobj.callBAF();
}

在上面例子中,我们让类模板B的实例B<long>成为类模板A的友元类,因此可以在实例B<long>中访问类模板A的私有变量data。需要注意的语法如下:

1
2
template<typename U> class B;	// 在类模板A的定义前增加类模板B的声明
friend class B<long>; // 在类模板A中声明友元类B<long>

2.类模板成为友元类模板

1
2
3
4
5
6
7
8
9
//template<typename U> class B;
template<typename T>
class A
{
//friend class B<long>;
template<typename> friend class B;
private:
int data;
};

3.类型模板参数成为友元类

让某个模板将其为类型的模板参数作为其友元类。

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
class A
{
friend T;
}

class B
{
void callBAF()
{
A<B> aobj; // 将类作为模板参数传入类模板A
}
}

友元函数

1.函数模板的实例成为友元函数

若将实例化后的模板函数func<int,int>作为普通类A的友元函数,语法如下:

1
2
friend void func<int,int>(int,int);	// 在普通类A中添加函数模板实例化对象为友元函数
template<typename U, typename V> void func(U val1, V val2); // 在普通类A定义前增加函数模板的声明,和类实例成为友元类一样

2.函数模板成为友元函数模板

若我们想让函数模板的所有实例都成为类A的友元函数,那么我们直接将函数模板作为类A的友元模板。

1
template<typename U, typename V> friend void func(U val1,V val2);	// 在普通类A中添加函数模板为友元模板

和类模板成为友元模板一样,在类A的的定义前毋需声明模板。

可变参模板

可变参模板运行模板定义中含有0到多个模板参数。

可变参函数模板

1
2
3
4
5
6
7
8
9
10
template<typename... T>		// typename...代表参数包
void myfunc(T... args) // T: 一包类型,args:一包形参。T后面加...代表T为可变参类型
{
cout << sizeof...(args) << endl; // 函数接收到的形参数量
cout << sizeof...(T) << endl; // 函数接收到的类型数量
}
int main()
{
myfunc(25, "mstifiy", 0.7);
}

sizeof...用于表示收到模板参数个数和类型数量,固定语法,C++11引入。

为了将接收到的参数解包,使用递归调用实现。

方式一:参数包展开函数+同名递归终止函数

1
2
3
4
5
6
7
8
9
10
11
void myfunc()		// 同名的递归终止函数
{
cout << "参数包解包递归函数终止" << endl;
}

template<typename T, typename... U>
void myfunc(T firstarg, U... otherargs) // 参数包展开函数
{
cout << "收到的参数值为:" << firstarg << endl;
myfunc(otherargs...); // 递归调用,实现参数的解包
}

递归终止函数必须在递归展开函数前定义,否则报错。

方式二:constexpr if

C++17标准中引入了编译期间if语句,即在编译时只有满足条件代码才会被编译。重写myfunc函数模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T, typename... U>
void myfunc(T firstarg, U... otherargs)
{
cout << "收到的参数值为:" << firstarg << endl;
if constexpr (sizeof...(otherargs) > 0)
{
myfunc(otherargs...);
}
else
{
cout << "参数包解包递归函数终止" << endl;
}
}

折叠表达式

折叠表达式的引入方便了需要所有可变参数参与计算才能得到的表达式结果的书写,即不用像上述一样将可变参数包解包再计算,而是可以直接通过简短的折叠表达式进行计算。折叠表达式一般有四种格式,每种格式都是用圆括号括起来的。参数从左侧开始计算叫左折,从右侧开始计算叫右折

1.一元左折

格式:(... 运算符 一包参数)

计算方式:(((参数1 运算符 参数2) 运算符 参数3) ··· 运算符 参数N)

2.一元右折

格式:(一包参数 运算符 ...)

计算方式:(参数1 运算符 (··· (参数N-1 运算符 参数N)))

3.二元左折

格式:(init 运算符 ... 运算符 一包参数)

计算方式:(((init 运算符 参数1) 运算符 参数2) ··· 运算符 参数N)

4.二元右折

格式:(一包参数 运算符 ... 运算符 init)

计算方式:(参数1 运算符 (··· (参赛N 运算符 init)))

可变参表达式

折叠表达式主要体现的是参数之间的运算,当需要让可变参数本身进行一些运算时,可以使用可变参表达式。

1
func((args * 2) ...) // 等价于func(arg1*2,arg2*2,...,argN*2)

格式:((可变参数自身运算) ...)

可变参类模板

模板参数列表语法和可变参函数模板一样,这里主要学习几种参数包的展开方式。

方式一:递归继承展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
using namespace std;

template<typename... args> // 类模板泛化版本
class myclasst
{
public:
myclasst()
{
printf("myclasst::myclasst()泛化版本执行了,this=%p\n", this);
}
};

template<> // 0个模板参数的特化版本
class myclasst<>
{
public:
myclasst()
{
printf("myclasst::myclasst()特殊的特化版本执行了,this=%p\n", this);
}
};

template<typename First, typename... Others>
class myclasst<First, Others...> :private myclasst<Others...> // 偏特化版本
{
public:
myclasst() :m_i(0)
{
printf("myclasst::myclasst()偏特化版本执行了,this = %p,sizeof...(Others)=%d\n", this, sizeof...(Others));
}

myclasst(First parf, Others... paro) :m_i(parf), myclasst<Others...>(paro...) // 在初始化前先调用myclasst<Others...>(父类)的构造函数进行初始化
{
printf("myclasst::myclasst(parf, ...paro)执行了,this = %p\n", this);
cout << "m_i = " << m_i << endl;
}
First m_i;
};

int main()
{
myclasst<int, float, double> myc(12, 13.5f, 30.0);
}

对于类模板可变参模板参数的展开操作,递归继承主要使用类模板的偏特化进行继承递归。对于非类型模板参数和模板模板参数,语法相似。

非类型模板参数包展开

1
2
3
4
5
6
template<double... args>	// 泛化版本
class A
...
template<double First, double... Others> // double也可以换成auto
class A<First, Others...> : private A<Others...> // 偏特化
...

模板模板参数包展开

1
2
3
4
5
6
template<typename T, template<typename> typename... Container>	// 泛化版本
class A
...
template<typename T, template<typename> typename FirstContainer, template<typename> typename... OthersContainer> // double也可以换成auto
class A<T, FirstContainer, OthersContainer...> : private A<OthersContainer...> // 偏特化
...

方式二:递归组合展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename... args> 
class A //泛化版本
...
template<typename First, typename... Others>
class A<First, Others...> // 偏特化
{
public:
A(First parf, Others... paro) : m_i(parf), m_o(paro...)
{
cout << m_i << endl;
}
First m_i;
A<Others...> m_o; //增加一个m_o变量
}

递归组合展开方式的思想是类的组合关系(一种包含关系)。

方式三:元组和递归调用展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
template<int count, int maxcount, typename... T> // count用于统计,从0开始,maxcount表示参数数量
class A
{
public:
static void func(const tuple<T...>& t)
{
cout << get<count>(t) << endl; // 将每个参数从元组中取出
A<count + 1, maxcount, T...>::func(t); // 递归调用,输出count+1,也就是下一个参数
}
}

// 偏特化版本,用于结束递归调用,必须存在
template<int maxcount, typename... T>
class A<maxcount, maxcount, T...>
{
public:
static void func(const tuple<T...>& t)
{
// 调用该偏特化模板时,count = maxcount(可由sizeof...计算),即元组中所有参数都已取出,参数包展开完成
}
}

template<typename... T>
void functuple(const tuple<T...>& t) //可变参函数模板
{
A<0, sizeof...(T), T...>::func(t); // count为0,从元组0号位开始
}

int main()
{
tuple<float, int, int> mytuple(1.0f, 100, 12);
functuple(mytuple);
}

总结

第二章模板基础知识已经学了4小节了,我个人感受是很杂和多,而且有一点难度的,这种难度主要来自于缺乏码代码的实践经验。把书翻得差不多之后,还是要多写多练才行,在动手写代码的同时,遗忘的知识点可以回过来翻书翻资料,这样学习吸收效果较佳。