《Effective Modern C++》学习笔记 - Item 1: 理解模板类型推导

一、介绍

对于一个复杂系统的用户来说,很多时候他们最关心的是它做了什么而不是它怎么做的。在这一点上,C++中的模板类型推导表现得非常出色。数百万的程序员只需要向模板函数传递实参,就能通过编译器的类型推导获得令人满意的结果,尽管他们中的大多数在被逼无奈的情况下,对于传递给函数的那些实参是如何引导编译器进行类型推导的,也只能给出非常模糊的描述。

如果那些人中包括你,我有一个好消息和一个坏消息。好消息是现在C++最重要最吸引人的特性auto是建立在模板类型推导的基础上的。如果你满意C++98的模板类型推导,那么你也会满意C++11的auto类型推导。坏消息是当模板类型推导规则应用于auto环境时,有时不如应用于template时那么直观。由于这个原因,真正理解auto基于的模板类型推导的方方面面非常重要。这项条款便包含了你需要知道的东西。

如果你不介意浏览少许伪代码,我们可以考虑像这样一个函数模板:

template<typename T>
void f(ParamType param);

它的调用通常是这样的

f(expr)

在编译期间,编译器使用expr进行两个类型推导:一个是针对T的,另一个是针对ParamType的。这两个类型通常是不同的,因为ParamType包含一些修饰,比如const和引用修饰符。举个例子,如果模板这样声明:

template<typename T>
void f(const T& param);         // ParamType是const T&

调用的形式像下面这样:

int x = 0;
f(x);                           //用一个int类型的变量调用f

T被推导为intparam被推导为const T&

我们可能很自然的期望T和传递进函数的实参expr是相同的类型,也就是,Texpr的类型。在上面的例子中,事实就是那样:xintT被推导为int。但有时情况并非总是如此,T的类型推导不仅取决于expr的类型,也取决于ParamType的类型。这里分为三种情况

  • ParamType是一个指针或引用,但不是万能引用,如T&
  • ParamType是一个万能引用引用,如T&&
  • ParamType既不是指针也不是引用, T

我们下面将分成三个情景来讨论这三种情况,每个情景的都基于我们之前给出的模板:

template<typename T>
void f(ParamType param);

f(expr);                        //从expr中推导T和ParamType

二、Case1:ParamType是一个指针或引用,但不是通用引用

最简单的情况是ParamType是一个指针或者引用,但非通用引用。在这种情况下,类型推导会这样进行:

  • 如果expr的类型是一个引用,忽略引用部分
  • 然后expr的类型与ParamType进行模式匹配来决定T

我们的模板声明如下:

#include <iostream>
#include <boost/type_index.hpp>

using boost::typeindex::type_id_with_cvr;
using std::cout;
using std::endl;

template<typename T>
void f(T& param){
    cout << "T's     type is " << type_id_with_cvr<T>().pretty_name() << endl;
    cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}
// 使用”type_id_with_cvr“来进行类型获取

我们声明如下变量:

int x=27;                       //x是int
const int cx=x;                 //cx是const int
const int& rx=x;                //rx是const int &

调用的形式如下:

f(x);		// T是int, ParamType是int&, param是int&
f(cx);		// T是const int, ParamType是const int&, param是const int&
f(rx);		// T是const int, ParamType是const int&, param是const int&

在第二个和第三个调用中,注意因为cxrx被指定为const值,所以T被推导为const int,从而产生了const int&的形参类型。这对于调用者来说很重要。当他们传递一个const对象给一个引用类型的形参时,他们期望对象保持不可改变性,也就是说,形参是reference-to-const的。这也是为什么将一个const对象传递给以T&类型为形参的模板安全的:对象的常量性constness会被保留为T的一部分。

在第三个例子中,注意即使rx的类型是一个引用,T也会被推导为一个非引用 ,这是因为rx的引用性(reference-ness)在类型推导中会被忽略。

如果我们将f的形参类型T&改为const T&,情况有所变化,但不会变得那么出人意料。cxrxconstness依然被遵守,但是因为现在我们假设paramreference-to-constconst不再被推导为T的一部分:

template<typename T>
void f(const T& param);         //param现在是reference-to-const

int x = 27;                     //如之前一样
const int cx = x;               //如之前一样
const int& rx = x;              //如之前一样

f(x);                           //T是int,param的类型是const int&
f(cx);                          //T是int,param的类型是const int&
f(rx);                          //T是int,param的类型是const int&

同之前一样,rxreference-ness在类型推导中被忽略了。
如果param是一个指针(或者指向const的指针)而不是引用,情况本质上也一样:

template<typename T>
void f(T* param);               //param现在是指针

int x = 27;                     //同之前一样
const int *px = &x;             //px是指向作为const int的x的指针

f(&x);                          //T是int,param的类型是int*
f(px);                          //T是const int,param的类型是const int*

三、Case2:ParamType是一个万能引用

万能引用参数的声明方式类似右值引用(T&&),当传入左值时其表现不同。这里给出简单总结:

  • 如果expr是左值,TParamType都被推导为左值引用。这是T会被推导为引用的唯一情况,而且尽管ParamType使用右值语法声明,其推导类型仍为左值引用,从而触发引用折叠int& && => int&
  • 如果expr是右值,按照Case 1正常规则
template<typename T>
void f(T&& param);              //param现在是一个通用引用类型
		
int x=27;                       //如之前一样
const int cx=x;                 //如之前一样
const int & rx=cx;              //如之前一样

f(x);                           //x是左值=>T是int&=>ParamType是int& &&=> param是int&
f(cx);                          //cx是左值,所以T是const int&,param类型也是const int&
f(rx);                          //rx是左值,所以T是const int&, param类型也是const int&
f(27);                          //27是右值,所以T是int,param类型就是int&&

四、Case3:ParamType既不是引用也不是指针

此时我们面对的是传值问题,这意味着 param 会是传入对象的一个复制,一个全新的对象。

  1. 如果expr的类型是引用,忽略引用部分。
  2. 如果忽略引用后exprconstvolatile,把那部分也忽略掉。
int x=27;                       //如之前一样
const int cx=x;                 //如之前一样
const int & rx=cx;              //如之前一样

f(x);                           //T和param的类型都是int
f(cx);                          //T和param的类型都是int
f(rx);                          //T和param的类型都是int

五、数组作为实参(expr)

上面的内容几乎覆盖了模板类型推导的大部分内容,但这里还有一些小细节值得注意,比如数组类型不同于指针类型,虽然它们两个有时候是可互换的。关于这个错觉最常见的例子是,在很多上下文中数组会退化为指向它的第一个元素的指针。 这种退化会导致下面这种代码是可编译的:

    const char name[] = "jack";
    const char* ptrToName = name;

但是如果将数组传递给下面这个模板会怎样呢?

template<typename T>
void f3(T param) {
    cout << "T's     type is " << type_id_with_cvr<T>().pretty_name() << endl;
    cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}

const char name[] = "jack"; 
f3(name); // T(const char*)

也就是说,这里无论传递数组还是指针都是等价的。

但是使用如下的模板便会区分出指针和数组的不同:

template<typename T>
void f2(T& param) {
    cout << "T's     type is " << type_id_with_cvr<T>().pretty_name() << endl;
    cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}

const char name[] = "jack";
const char* ptrToName = name;
int x = 10;
const int* px = &x;
f2(name);			// T(const char [5]), Param(char const (&)[5])
f2(ptrToName);		// T(const char*), Param(const char*&) 
f2(px);				// T(const int*), Param(const int*)

可以看到T被推导为数组的类型,也就是expr的类型, 形參(Param)被推导为char const (&)[5]类型。

这种引用数组的功能,可以让我们实现在编译期间推导出数组长度的功能:

template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept{			// 少见
    return N;
}

const char name[] = "jack";
cout << arraySize(name) << endl; // 5

六、 函数作为实参(expr)

c++中不止数组类型会发生退化,函数类型同样会发生退化, 我们对于数组退化的讨论同样适用于函数:

template<typename T>
void f2(T& param) {
    cout << "T's     type is " << type_id_with_cvr<T>().pretty_name() << endl;
    cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}

template<typename T>
void f3(T param) {
    cout << "T's     type is " << type_id_with_cvr<T>().pretty_name() << endl;
    cout << "Param's type is " << type_id_with_cvr<decltype(param)>().pretty_name() << endl;
}

void someFunc(int, double){}

f2(someFunc); // T(void (int, doble)), Param(void (&)(int, double))
f3(someFunc); // T(void (*)(int, double)), Param(void (&)(int, double))

函数引用和函数指针都可以调用, 如param(1, 1.1)

七、总结

  1. 在模板类型(T)推导时,有引用的实参(expr)会被视为无引用,他们的引用会被忽略
  2. 对于通用引用的推导,左值实参会被特殊对待,左值会导致模板类型被推导为左引用(比如expr为int类型左值,那么T会被推导为int&
  3. 对于传值类型的推导,实参exprconstvolatile属性会被忽略
  4. 对与传值类型得模板推导,数组或者函数名会退化为指针

参考

  1. 翻译:理解模板类型推导
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值