技术: C++11 模板别名探究

本文涉及到模板别名, 类型别名, 以及模板的模板参数问题.

模板别名(template alias) 或者 template typedef, 以及类型 type alias

两者都是借助using关键字, 先花大力气理解template typedef,
之后再补充说下type alias以及默认模板参数问题

可能会说一些元编程中比较晦涩的问题, 尽量说清楚


模板别名

这里面有些问题, 整的我一度有些奔溃; 因为涉及到模板, 一旦出问题, 调用深度大, 参数多也不好理解.

比如模板模板参数问题, 比如说别名中还要使用模板类型问题

我尝试着说清楚, 一方面检验我的理解, 另外一方面说一些这里面的一些技巧或者已经可以舍弃了的C++03的技巧.

模板出现在typedef中

就是模板参数和typedef混合了, 或者说,

typedef的增强, 你可以在typedef的时候, 同时运用模板参数.

重点其实是落在了typedef上, 加入模板参数的 typedef, 更加强大的typedef.

C++03的时候, 我们是这么做的:

1
2
3
typedef std::map<std::string, int> map_t;  
//...
typedef std::map < std::string, std::string > map_str;

完全OK, 但是如果这样做:

1
2
3
4
5
template <typename T>  
typedef std::map<std::string, T> map;

map<int> map_i;
map<std::string> map_str;

直接报错了, 在typedef的内容中, 使用模板参数, 03语法不支持, 所以一般我们会借助模板类型定义, 函数模板或者类模板, 甚至 functor 模板
例如:

1
2
3
4
5
6
7
8
9
template <typename T>  
struct alias_map
{
typedef std::map<std::string, T> map;
//using map = std::map<std::string, T>; //具体参考下面的type alias
};

alias_map<int>::map map_t; //借助了 alias_map 类模板
alias_map<int>::map map_str;

也就是说, 此时能具体的指定typedef的类型, 还是要借助类(结构体)模板, 还不能直接和typedef结合.

C++11 直接可以使用别名是指定模板, 不需要间接的, 通过定义一个类型才能用模板机制.

但是又不能破坏以前的规则, 所以干脆不用 typedef 了, 使用using吧, 即

1
2
3
4
5
template <typename T>  
using alias_map = std::map < std::string, T > ;

alias_map<int> map_t;
alias_map<std::string> map_str;

更多的例子, 比如:

1
2
template<typename T>  
using func_using = void(*func)(T, T);

模板模板参数问题

想从模板参数中直接取出内部类型怎么办? 即模板参数特化时具体类型的参数(即模板模板参数).

从函数模板中的模板参数中取出具体的参数内部的类型

举个例子, 有一个函数模板, 例如:

1
2
3
4
template<typename T>
void test(T t){
//...具体实现, 实现中需要用到t中内部的类型, 可能是元素类型
}

而我具体调用的时候, 是这样的

1
2
list<string> li;
void test (li);

我需要传递li对象, 是因为我想获取string类型

所以可以知道, 函数模板特化出来的模板函数是:

1
void test(list<string> t);

那么问题来了, 如果我的最初的函数模板里面的实现代码中, 需要T中的具体模板参数怎么办? 此处Tlist<string> 需要拿到T中的模板参数string
例如:

1
2
3
4
5
template<typename T>
void test(T t){
//具体实现
//抱歉拿不出来.
}

尝试这么写, 在声明模板参数的时候, 就声明模板为模板参数

即模板套模板

1
2
3
4
5
6
template <typename T, template<class> class P>
class X
{
private:
P<T> p; //注意这里用的是P模板, 下面分析时用到了.
};

相当于在外层模板里面又嵌套了一层内层模板, P作为外层模板, 相当于上面的list<string>类型,
T则作为内层模板类型&参数, 这个案例中相当于string.

等价于template <typename T, template<class T> class P>, 但 T 可以省略, 因为外层模板P没有使用内层模板参数T.

调用的时候, 也简单X<string, list>, 当然如果上面的内层模板我指定的话, 完整应该写成X<string, list<string>>.

直接就拿到了内层的模板参数string了. 抱歉, 编译器报错了: (模板的二次编译问题)

第一次检查模板语法OK, 但是一旦根据你的调用逻辑进行二次编译的时候, 就报错了.

第二个模板定义和实际调用不匹配, 需要template<class T> class P, 实际拿到的却是template<class _Tp, class _Alloc> class std::vector.
其实很奇怪, 因为class _Alloc有默认值, 翻看源码知道class _Alloc=std::allocator<_Tp>.

只能说编译器不够智能吧, 根据我们的调用, 第二参数string, 完全是可以省略默认模板参数的.

关键是调用时不对, 调用时如果能指定 allocator 就很好了

但是上面 class X里面成员, 我们是这么用的P<T> p, 对于模板模板参数而言, 编译器不知道第二个模板参数有默认值, 尤其是第二个模板参数有默认值的情况.

嵌套的嵌套来拿到模板参数的参数, 编译器确实没有办法了

但是对于最开始的设计方式, 标准库的容器的话, 可以尝试这样拿出来:
容器--> 迭代器 --> value_type

1
2
3
4
5
6
7
template<typename T>
void test(T t){
//具体实现
typedef typename iterator_traits<typename T::iterator>::value_type Valtype;
//或者
typedef typename T::iterator::value_type Valtype;
}

此时拿到了T内部元素的类型了, 即typename iterator_traits<typename T::iterator>::value_type别名为Valtype;

对应上面的例子, 就是string. 如果你不定义别名, 那么就要写很长的名字, 用于函数内部代码. 此处重点不在typedef , 在于拿到模板参数T内部的元素类型.

这只是对于标准库可以这么traits萃取到类型特征.

一般性情况如何解决?
我就想这么用, 在模板里面再套一个模板, 这个时候, 就用 template typedef 吧(还是using语法那个), 妙不可言.

我还是想这么用X<string, list<string>>, 定义的时候, 形式也不变, 即:
(内部模板的模板参数, 是以第一个模板参数为参数的)

1
2
3
4
5
6
7
template <typename T, template<class> class P>
//等价于template <typename T, template<class T> class P>, 但 T 可以省略.
class X
{
private:
P<T> p; //注意这里用的是P模板, 下面分析时用到了.
};

关键是在调用时, 调用的时候借用一下模板别名:

1
2
3
4
template<typename T>
using Lst=list<T, allocator<T>>;

X<string, Lst<string>> x; //注意没有指定第二个模板参数哦

超级难题, 一下子解决了… (), 苦笑不得啊.

展开X<string, lis<string, allocator<string> > >, 为什么是这种展开?
因为对于模板模板参数而言, 编译器不知道第二个模板参数有默认值.

拿到内部类型干啥?

你可能看到typedef basic_string<char> string 但是其实他默认是单字节字符, 直接拿有点儿困难.

当我要拿到string内部的类型用于函数&类模板内部时(可能是单字节,双字节, 宽字符等), 我依旧只能:

1
2
template<class CharT>
using common_string=std::basic_string<CharT, std::char_traits<charT> >;

此时调用如下:

1
X<wchar, common_string<wchar>>

才能特化出我要的模板类&函数版本.

总结

优点

简单说, 就是这样的东西:

1
2
3
4
5
6
template <typename T>
using Vec = std::vector<T>;

//以后直接使用Vec即可.
Vec<int> v;
//相当于 std::vector<int> v;

方便了很多:

  • 代替了宏定义define, 只是宏定义不带有类型, 且是预处理的;
    宏定义很多时候, 达不到效果的, 特别是在模板中
  • 增强了typedef

好比说, 你用typedef, 例如:

1
typedef std::vector<int> Vec

那么后面使用的时候, 就直接Vec v;, 根本不必要, 也不能传递泛型参数了.

其实我觉得最大的优点是解决了模板中嵌套模板的难题. (template template在使用的时候, 即编译器二次编译时, 对调用形式要求很高)

限制

一旦你这样using了别名, 你就放弃了模板的特化(全特化或者偏特化), 也就是说, 你只能拿到, 而不能拿来定义模板了.
(上面的例子, 也说明了这个问题, 我只是在使用的场景中才用到了模板别名)

模板别名, 只用在使用的场景, 不用于定义的场景.

下面补充说明一下type alias.

类型别名

type alias这个只能算是个小小的方便, 例如:

1
typedef void (*func)(int, int);

现在也可以用 using 来写, 而且更加简单, 更加突出类型的地位.

1
using func = void(*)(int, int);

使用起来完全当做类型去考虑即可:

1
2
func fn = f1;
fn();

也就是说, 彻底侧重于,以类型为中心, 我这么写是在定义类型, 而不是以对象&变量为中心;

其他用途:

1
2
3
4
5
6
template <typename T>
struct X{
using value_type = T; //给模板参数起别名
//相当于
//typedef T value_type;
};

默认模板参数

在 C++11 中, 支持默认模板参数, 例如:

1
2
3
4
5
template<class T, class U = int, U n= 0>
struct Foo
{
//
};

在函数模板中当所有模板参数都有默认参数时,函数的调用就如同普通的函数调用,

但是对于类模板而言,哪怕所有模板参数都有默, 认构造函数在使用时还是必须在模板名后跟随<>来实例化

C++11中函数的默认模板参数在使用规则上和其他的默认参数也有一些区别,普通函数的默认参数必须写在参数列表的最后,而函数的模板参数就没有这个限制.

因此当使用默认模板参数和模板参数自动推导时就显示十分灵活,可以指定函数中的一部分参数是默认参数,另一部分采用自动推导(其实是从左到右顺序, 优先匹配).

1
2
3
4
5
template <typename R = int, typename U>
R func(U val)
{
//...
}

建议在使用的时候尽量还是价格默认模板参数写在模板参数的末尾

文章目录
  1. 1. 模板别名
    1. 1.1. 模板出现在typedef中
    2. 1.2. 模板模板参数问题
    3. 1.3. 总结
      1. 1.3.1. 优点
      2. 1.3.2. 限制
  2. 2. 类型别名
  3. 3. 默认模板参数
|