深入理解C++11(二)

深入理解C++11(二)

模板的细节改进
C++11改进了编译器的解析规则,尽可能地将多个右尖括号(>)解析成模板参数结束符,方便我们编写模板相关的代码。
1.2.1 模板的右尖括号
在C++98/03的泛型编程中,模板实例化有一个很烦琐的地方,那就是连续两个右尖括号(>>)会被编译器解释成右移操作符,而不是模板参数表的结束。
代码清单1-6 C++98/03中不支持连续两个右尖括号的示例:

template <typename T>
struct Foo
{
typedef T type;
};
template <typename T>
class A
{
// ...
};
int main(void)
{
Foo<A<int>>::type xx; // 编译出错
return 0;
}

使用gcc编译时,会得到如下错误提示:

error: '>>' should be '>>' within a nested template argument list
Foo<A<int>>::type xx;

现在在C++11中,这种限制终于被取消了。在C++11标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出“>>”是一个右移操作符还是模板参数表的结束标记(delimiter,界定符)。
不过这种自动化的处理在某些时候会与老标准不兼容,比如下面这个例子:

template <int N>
struct Foo
{
// ...
};
int main(void)
{
Foo<100 >> 2> xx;
return 0;
}

在C++98/03的编译器中编译是没问题的,但C++11的编译器会显示:

error: expected unqualif?ied-id before '>' token
Foo<100 >> 2> xx;

解决的方法是这样写:

Foo<(100 >> 2)> xx; // 注意括号

这种加括号的写法其实也是一个良好的编程习惯,使得在书写时倾向于
写出无二义性的代码。

1.2.2 模板的别名
大家都知道,在C++中可以通过typedef重定义一个类型:

typedef unsigned int uint_t;

被重定义的类型名叫“typedef-name”。它并不是一个新的类型,仅仅只是原有的类型取了一个新的名字。因此,下面这样将不是合法的函数重载:

void func(unsigned int);
void func(uint_t); // error: redefinition

使用typedef重定义类型是很方便的,但它也有一些限制,比如,无法重定义一个模板。想象下面这个场景:

typedef std::map<std::string, int> map_int_t;
// ...
typedef std::map<std::string, std::string> map_str_t;
// ...

我们需要的其实是一个固定以std::string为key的map,它可以映射到int或另一个std::string。然而这个简单的需求仅通过typedef却很难办到。
因此,在C++98/03中往往不得不这样写:

template <typename Val>
struct str_map
{
typedef std::map<std::string, Val> type;
};
// ...
str_map<int>::type map1;
// ...

一个虽然简单但却略显烦琐的str_map外敷类是必要的。这明显让我们在复用某些泛型代码时非常难受。
现在,在C++11中终于出现了可以重定义一个模板的语法。请看下面的示例:

template <typename Val>
using str_map_t = std::map<std::string, Val>;
// ...
str_map_t<int> map1;

这里使用新的using别名语法定义了std::map的模板别名str_map_t。比起前面使用外敷模板加typedef构建的str_map,它完全就像是一个新的map类模板,因此,简洁了很多。

实际上,using的别名语法覆盖了typedef的全部功能。先来看看对普通类型的重定义示例,将这两种语法对比一下:

// 重定义
unsigned int
typedef unsigned int uint_t;
using uint_t = unsigned int;
// 重定义
std::map
typedef std::map<std::string, int> map_int_t;
using map_int_t = std::map<std::string, int>;

可以看到,在重定义普通类型上,两种使用方法的效果是等价的,唯一
不同的是定义语法。

typedef的定义方法和变量的声明类似:像声明一个变量一样,声明一个重定义类型,之后在声明之前加上typedef即可。这种写法凸显了C/C++中的语法一致性,但有时却会增加代码的阅读难度。比如重定义一个函数指针时:

typedef void (*func_t)(int, int);

此处说明,例如:
typedef char* PCHAR;char* 指针dao起了个别名PCHAR。
PCHAR pa, pb;    声明shuchar* 型指针就可以用PCHAR 来声明。
typedef void (*funcptr)(void)void (* )(void)  起了个别名:funcptr。
声名函数指针:void (* )(void),就可以用funcptr 来声明,即: funcptr  fun1;
用void (*funcptr)(void);来直接声明函数指针也行。但是不能写成 funcptr  fun1;
用typedef 可以简化声明,也更清晰

与之相比,using后面总是立即跟随新标识符(Identifier),之后使用类似
赋值的语法,把现有的类型(type-id)赋给新类型:

using func_t = void (*)(int, int);

从上面的对比中可以发现,C++11的using别名语法比typedef更加清晰。因为typedef的别名语法本质上类似一种解方程的思路。而using语法通过赋值来定义别名,和我们平时的思考方式一致。

下面再通过一个对比示例,看看新的using语法是如何定义模板别名的。

/* C++98/03 */
template <typename T>
struct func_t
{
typedef void (*type)(T, T);
};
// 使用 func_t 模板
func_t<int>::type xx_1;
/* C++11 */
template <typename T>
using func_t = void (*)(T, T);
// 使用 func_t 模板
func_t<int> xx_2;

从示例中可以看出,通过using定义模板别名的语法,只是在普通类型别名语法的基础上增加template的参数列表。使用using可以轻松地创建一个新的模板别名,而不需要像C++98/03那样使用烦琐的外敷模板。

需要注意的是,using语法和typedef一样,并不会创造新的类型。也就是说,上面示例中C++11的using写法只是typedef的等价物。虽然using重定义的func_t是一个模板,但func_t定义的xx_2并不是一个由类模板实例化后的类,而是void(*)(int,int)的别名。
因此,下面这样写:

void foo(void (*func_call)(int, int));
void foo(func_t<int> func_call); // error: redefinition

同样是无法实现重载的,func_t只是void(*)(int,int)类型的等价物。
细心的读者可以发现,using重定义的func_t是一个模板,但它既不是类模板也不是函数模板(函数模板实例化后是一个函数),而是一种新的模板形式:模板别名(alias template)。

其实,通过using可以轻松定义任意类型的模板表达方式。比如下面这样:

template <typename T>
using type_t = T;
// ...
type_t<int> i;

type_t实例化后的类型和它的模板参数类型等价。这里,type_t<int>将等
价于int

1.2.3 函数模板的默认模板参数

当默认模板参数和模板参数自动推导结合起来使用时,书写显得非常灵活。我们可以指定函数中的一部分模板参数采用默认参数,而另一部分使用自动推导,比如下面的例子:

template <typename R = int, typename U>
R func(U val)
{
ret val
} int main(void)
{
func(123);
return 0;
}

但需要注意的是,在调用函数模板时,若显示指定模板的参数,由于参数填充顺序是从右往左的,因此,像下面这样调用

func<long>(123); // func的返回值类型是 long

函数模板func的返回值类型是long,而不是int,因为模版参数的填充顺序从右往左,所以指定的模版参数类型long会作为func的参数类型而不是func的返回类型,最终func的返回类型为long。这个细节虽然简单,但在多个默认模板参数和模板参数自动推导穿插使用时很容易被忽略掉,造成使用时的一些意外。

另外,当默认模板参数和模板参数自动推导同时使用时,若函数模板无法自动推导出参数类型,则编译器将使用默认模板参数;否则将使用自动推导出的参数类型。请看下面这个例子:

template <typename T>
struct identity
{
typedef T type;
};
template <typename T = int>
void func(typename identity<T>::type val, T = 0)
{
// ...
} int main(void)
{
func(123); // T -> int
func(123, 123.0); // T -> double
return 0;
}

在例子中,通过identity外敷模板禁用了形参val的类型自动推导。但由于
func指定了模板参数T的默认类型为int,因此,在func(123)中,func的val参
数将为int类型。而在func(123123.0)中,由于func的第二个参数123.0double类型,模板参数T将优先被自动推导为double,因此,此时val参数将为
double类型。
这里要注意的是,不能将默认模板参数当作模板参数自动推导的“建议”,
因为模板参数自动推导总是根据实参推导来的,当自动推导生效时,默认模
板参数会被直接忽略。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值