1.2、模板模板参数改进
一、什么是模板模板参数?为什么C++17要改进它?
先回顾一下:模板模板参数就是模板的参数是另一个模板。比如:
template<typename T, template<typename, typename> class Cont>
class MyContainer {
Cont<T, std::allocator<T>> data;
// ...
};
这里Cont
是一个模板模板参数,要求传入的模板必须是一个带两个类型模板参数的类模板,比如std::vector
。
在C++14及之前,模板模板参数声明时只能用class
关键字,而且模板参数的匹配非常严格:传入的模板参数列表必须完全匹配声明的模板参数列表,否则编译失败。
C++17改进点
- • 允许用
typename
替代class
声明模板模板参数
以前只能写template <typename T, template<class, class> class Cont>
,现在可以写成template <typename T, template<typename, typename> typename Cont>
,语义更统一,代码更清晰。 - • 模板模板参数匹配更宽松,支持默认模板参数匹配
之前如果模板模板参数声明的是两个模板参数,但传入的模板带有默认参数,匹配会失败。C++17修正了这个问题,允许带默认模板参数的模板匹配模板模板参数,即使默认参数没有显式写出,也能匹配成功。
二、设计哲学与底层原理剖析
设计哲学:让模板匹配更灵活,减少模板使用障碍
- • 统一语法:
typename
和class
在模板类型参数中本质等价,C++17允许模板模板参数也用typename
,统一了语言风格,减少认知负担。 - • 匹配宽松:模板模板参数匹配规则放宽,支持默认模板参数的匹配,减少模板参数书写的繁琐,提升模板代码的复用性。
- • 兼容性提升:解决了旧标准下模板模板参数匹配失败的问题,避免了大量模板代码因默认参数而无法匹配的尴尬。
底层原理:模板参数匹配规则的调整
模板模板参数匹配的本质是“模板模板参数列表”和“实参模板参数列表”的形状(参数个数和类型)是否兼容。C++17允许:
- • 实参模板参数列表可以多于模板模板参数声明的参数个数,只要多出的参数都有默认值。
这样,模板模板参数声明template<typename, typename>
可以匹配实参模板template<typename, typename=std::allocator<T>>
。
这背后是标准委员会对模板匹配规则的细化和放宽,兼顾了灵活性和类型安全。
三、深度案例解析
案例1:用typename
替代class
声明模板模板参数
#include <vector>
#include <list>
#include <iostream>
template<typename T, template<typename, typename> typename Cont>
class MyContainer {
public:
MyContainer(std::initializer_list<T> il) : data(il) {}
void print() const {
for (const auto& elem : data) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
private:
Cont<T, std::allocator<T>> data;
};
int main() {
MyContainer<int, std::vector> vecCont{1, 2, 3, 4, 5};
vecCont.print();
MyContainer<std::string, std::list> listCont{"a", "b", "c"};
listCont.print();
return 0;
}
解析:
- • 这里
Cont
用template<typename, typename> typename Cont
声明,替代了传统的class
关键字,语义完全等价。 - •
std::vector
和std::list
都带两个模板参数(元素类型和分配器),符合模板模板参数要求。 - • 代码简洁且语义清晰,体现了C++17对模板模板参数声明的语法改进。
案例2:默认模板参数匹配的灵活性
#include <vector>
#include <iostream>
template<typename T, template<typename, typename = std::allocator<T>> class Cont>
class Wrapper {
public:
Wrapper(std::initializer_list<T> il) : data(il) {}
void print() const {
for (const auto& elem : data) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
private:
Cont<T> data;
};
int main() {
Wrapper<int, std::vector> w{10, 20, 30};
w.print();
return 0;
}
解析:
- •
Wrapper
模板模板参数声明了第二个模板参数带默认值= std::allocator<T>
。 - • C++17允许传入的模板
std::vector
带默认模板参数匹配成功。 - • 这在C++14中会编译失败,因为模板模板参数和实参模板参数列表不完全匹配。
- • 这里
Cont<T>
实际展开为std::vector<T, std::allocator<T>>
,使用了默认参数。
案例3:C++17前后匹配差异导致的歧义问题
#include <vector>
#include <iostream>
template<typename T, typename Alloc>
void func(const std::vector<T, Alloc>&) {
std::cout << "func with std::vector<T, Alloc>" << std::endl;
}
template<typename T, template<typename> class Vector>
void func(const Vector<T>&) {
std::cout << "func with Vector<T>" << std::endl;
}
int main() {
std::vector<int> v;
func(v); // C++14编译通过,C++17编译报错:调用歧义
return 0;
}
解析:
- • C++14中,
std::vector
有两个模板参数,第二个带默认值,模板模板参数template<typename> class Vector
不匹配,调用第一个func
。 - • C++17中,默认模板参数匹配规则放宽,两个
func
都成为候选,导致调用歧义。 - • 这体现了模板模板参数匹配规则改进带来的副作用,提醒我们要注意重载设计。
四、常见错误与注意事项
- • 误用
class
与typename
C++17允许模板模板参数用typename
替代class
,但混用时需保持一致,避免代码风格混乱。 - • 默认模板参数匹配导致的歧义
如案例3所示,默认参数匹配可能引发重载歧义,设计函数模板时应避免模糊匹配。 - • 模板模板参数参数列表不匹配
传入模板参数列表与模板模板参数声明不兼容仍然会编译失败,尤其是非默认参数部分。 - • 对非类型模板参数的限制
模板模板参数只能匹配模板类型参数,不能匹配非类型模板参数或模板模板参数本身。
五、总结与独到观点
C++17对模板模板参数的改进,虽是细节,却极大提升了模板编程的灵活性和一致性。它让模板模板参数的声明更统一,匹配更宽松,减少了历史遗留的繁琐限制。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)
阅读剩余
版权声明:
作者:讳疾忌医-note
链接:https://www.1217zy.vip/archives/1012
文章版权归作者所有,未经允许请勿转载。
THE END