在开发的过程中遇到一个问题,就是用来实现一个多数据联合存储的问题。考虑到后期的简单扩展,考虑使用了双重模板,当时一时兴起,甚至想如果用三重模板怎么样?搞了半天,发现使用那个没有啥意义,徒然引入了更多的问题。
这里先从原来的模板的模板参数相关内容铺垫:
//模板嵌套
template struct A {};
template struct B {};
// function nest
template class MyData> constexpr auto Test0(MyData md) { return 0; }
template class MyData> constexpr auto Test(MyData md) { return 0; }
// template template parmsater
auto c = B>();
template struct BB {};
template typename N> struct AB {
AB() { std::cout << "AB is construct!" << std::endl; }
};
template typename N> struct AB1 {
AB1() { std::cout << "AB1 is construct!" << std::endl; }
};
template typename N> struct ABC {
ABC() { std::cout << "ABC is construct!" << std::endl; }
};
int main() {
AB ab;
AB1 ab1;
ABC abc;
Test0(A{});
Test(B>{});
return 0;
}
上述的代码从简单的基础模板到初步的嵌套模板到函数模板的模板参数到类模板的模板参数,这算是一个基础的应用,同时,也对原来模板参数中“auto c = B()”进行完善说明。
下面正式引入问题:
1、模板的模板参数应用
2、模板的模板参数再嵌套
template > struct DataVec {
con vec;
void add(T t) {
vec.emplace_back(t);
std::cout << "cur vec index 0:" << vec[0] << std::endl;
}
};
template class con> class DataList1 {
public:
con list;
void add(T t) {
list.emplace_back(t);
std::cout << "cur vec index 0:" << list[0] << std::endl;
}
};
template class con = std::list> class DataList {
public:
con list;
void add(T t) {
list.emplace_back(t);
std::cout << "cur vec index 0:" << list[0] << std::endl;
}
};
template class con = std::list> class DataListQueue {
public:
con list;
void add(T t) {
list.add(t);
// std::cout << "cur vec index 0:" << list[0] << std::endl;
}
};
template class con = std::list> class DataListVec {
public:
con list;
void add(T t) {
list.add(t);
// std::cout << "cur vec index 0:" << list[0] << std::endl;
}
};
// ERROR before C++17,must under comment code
// template >
// class CONT = std::deque>
template typename con = std::queue> class DataQueue {
public:
con queue;
void add(T t) {
queue.emplace_back(t);
std::cout << "cur vec index 0:" << queue[0] << std::endl;
// std::cout << "cur vec index 0:"
// << "queue[0] " << std::endl;
}
};
template typename con = std::queue> class DataQueue1 {
public:
con queue;
void add(T t) {
queue.emplace(t);
std::cout << "cur vec index 0:" << queue.front() << std::endl;
// std::cout << "cur vec index 0:"
// << "queue[0] " << std::endl;
}
};
int main() {
DataVec> ddv;
DataQueue ddqv;
ddqv.add(101);
DataList1 ddlv;
ddlv.add(111);
DataVec dv;
dv.add(10);
DataListVec dldv;
dldv.add(321);
DataListQueue dldq;
dldq.add(1110);
return 0;
}
这个代码和相关资料中使用模板的模板参数和侯捷老师认为不是模板的模板参数的例子相似,但是解决了使用此类模板进行了再嵌套(类似于Stack中再使用类似Stack的方式)。后来考虑,其实这个意义不大,但在某些情况下可能会有用。
这里面需要注意的两个问题:
1、在实现类似Stack的类模板中,在早期标准里需要实现默认分配(Alloc),但在新标准(C++17,但本机测试C++11也没问题)不需要处理。
2、注意使用确定模板类型做为参数和模板的模板参数的不同即类似DataVec这种情况。
3、模板的模板参数内层的参数如果不使用可以省略名称,只保留typename即可。另外,模板的模板参数第二个参数在C++17前只能使用class,C++11后虽然可以使用别名,但真正可以使用typename,只有在C++17的标准后。
4、注意模板参数的作用域即访问层次。模板参数的内层参数可以使用外层参数,但外层无法直接使用内层参数。这和在正常情况下类可以访问成员函数但不可以访问其内部变量类似。
在上面的基础上,引入三重模板:
template class con = std::vector> struct DataCon {
con c;
void add(T t) {}
};
template class scon = std::vector> typename con>
struct DataCon3 {
con c;
con c1;
void add(T t) { /*c.c.emplace_back(t);*/
c1.c.emplace_back(t);
}
void c2add(T t) { c.add(t); }
};
int main()
{
DataCon3 dc3;
dc3.add(210);
dc3.c2add(10);
return 0;
}
其实,在这个问题解决的并不完美,应该增加一下SFINAE或者Concepts的处理和判断,这样会使这个模板类的应用更安全,但考虑到此处只是一个解决问题的例程,就不再继续完善下去,有兴趣的小伙伴可以自己试试。
在编译的过程中可以使用一些工具如:
clang++ -std=c++11 -Xclang -ast-print -fsyntax-only main.cpp
来查看一些模板的中间代码,但试了下,效果不是很好。也可以找其它一些工具来使用,但效果就不好说了。
其实这么做就已经有画蛇添足的味道了,但为此付出的时间可不少。
在软件工程中,无论是设计、架构还是模块开发,甚至到具体的每一个小功能开发,整体的实现方向是简便为主。也就是说,在这些过程中,如果出现过于复杂的实现,那么极大概率是设计和思路出现了偏差。举一个简单例子,如果实现某个功能需要使用三级指针或者某个函数写了三四千行,除了极个别的特殊情况外,应该是思路出了偏差。
其实软件重构就是对原来的设计思路和实现方法的一种重新思考和再实现的过程。其中软件重构的一些思想,就是从宏观上指导重构者重构的目标和方向,这些都有更专业的书籍,此处就不再狗尾续貂,有时间,还是推荐大家认真读一下相关书籍和资料。
以本文为例,如果用到了超过两层的的模板参数,首先考虑的不是怎么完美的实现它,而是考虑是不是自己的设计和指导思路出问题;其次才是如何把问题逆向思考简化再考虑实现。如果真遇到一些特殊的情况,也需要认真的考虑是否有必要这样做,再决定是否去实现它。总之,越是复杂或者所谓精巧的实现,都意味着后期维护成本的急剧提升,绝大多数情况下是得不偿失的。