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

从一个基础的函数模板开始:

template<typename T>
void f(ParamType param);
// 伪代码,ParamType通常是T加上装饰符(adornments,如const, &, *等)

简单具化ParamType:

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

int x = 0;
f(x); // T被推导为int

看起来,T 只根据 expr 推导。然而实际上,T还 与 ParamType 的形式有关,分三种情况:
1. ParamType 是指针或引用类型,但不是万能引用(universal reference,详见Item 24,现在只需知道它是种和左值右值都不同的引用)。
2. ParamType 是万能引用。
3. ParamType 既不是指针也不是引用。

Case 1. ParamType 是指针或引用类型,但不是万能引用

  • 此时,类型推导的规则为:
    1. 如果 expr 的类型是引用,忽略引用部分。
    2. 然后将 expr 的类型匹配到 ParamType 上来决定 T 的类型。

举例:

template<typename T>
void f(T& param);	 // 模板的ParamType为引用

int x = 27; 		// x是int
const int cx = x; 	// cx是const int
const int& rx = x;	// rx是对x的常引用

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

VS2022运行结果:
在这里插入图片描述
(关于如何打印正确的类型名,详见Item 4。实测VS中使用typeid(T).name()会丢失类型的const信息)

  • 由上可见向 T& 参数的函数传一个 const 对象是安全的,因为 const 性质会被保留到推导出的 T 的类型中。
  • 如果将 ParamTypeT& 改为 const T&,三个调用中 T 均被推导为 int
  • 指针与引用的情况基本相同。

Case 2. ParamType 是万能引用

  • 万能引用参数的声明方式类似右值引用(T&&),当传入左值时其表现不同。Item 24讲述了完整原理,这里给出简单总结:
    1. 如果 expr 是左值,TParamType 都被推导为左值引用。这是 T 会被推导为引用的唯一情况,而且尽管 ParamType 使用右值语法声明,其推导类型仍为左值引用。
    2. 如果 expr 是右值,按照Case 1正常规则。

举例:

template<typename T>
void f(T&& param); // param是万能引用

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

f(x); 	// x是左值, 所以T是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&&

VS2022运行结果:
在这里插入图片描述

Case 3. ParamType 既不是指针也不是引用

  • 此时我们面对的是传值问题,这意味着 param 会是传入对象的一个复制,一个全新的对象。
    1. 如果 expr 的类型是引用,忽略引用部分。
    2. 如果忽略引用后 exprconstvolatile,把那部分也忽略掉。(关于 volatile,详见Item 40)

举例:

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

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

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

VS2022运行结果:
在这里插入图片描述

  • 尽管入参 cxrxconst 值, param 并不是 const,因为它已经是和前者完全无关的新对象了。volatile 同理。
  • 考虑指向常量对象的常指针:
template<typename T>
void f(T param);

// char*左侧的const说明ptr指向的对象本身(即那个字符串)不能被改变
//		右侧的const说明ptr不能指向别的对象或被设为null
const char* const ptr = "Fun with pointers";

f(ptr); 
  • 此时,ptr 被按字节复制到 param 中,使其右侧的 const 被忽略,而保留左侧。即 param 是指向的对象本身不可修改,但指向什么对象可以修改的指针(const char*)。
    在这里插入图片描述

数组参数

  • 数组类型参数与指针类型不是完全相同的
  • 在类型推导中,传入的数组变量的确会被推导为指针类型:
template<typename T>
void f(T param); 

const char name[] = "J. P. Briggs";
f(name); // name是数组,但推导的类型是const char*
  • 有趣的是,如果将参数声明为对数组的引用,这时推导出的类型将是数组的真实类型!
template<typename T>
void f(T& param); 

const char name[] = "J. P. Briggs";
f(name); // T被推导为const char[13], param被推导为const char (&)[13]!

VS2022运行结果:
在这里插入图片描述

  • 利用这个特性甚至可以写出一个编译期的萃取数组长度的模板函数:
// constexpr见Item 14,noexcept见Item 15
// 这里我们只关注数组元素个数,所以不需要参数名
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
	return N;
}

char name[] = "123"; 		// 返回4
int keyVals[] = {1, 2, 3}; 	// 适用于大括号初始化的数组,返回3

函数参数

  • 函数类型的参数也会退化(decay)为函数指针。以上对于数组的讨论都适用于函数指针:
void someFunc(int, double);

// type is void(int, double)
template<typename T>
void f1(T param);
template<typename T>
void f2(T& param);
f1(someFunc); 	//param类型为void (*)(int, double)
f2(someFunc);	//param类型为void (&)(int, double)

总结

  1. 在模板类型推导中,入参的引用修饰符会被忽略。(reference-ness is ignored)
  2. 推导万能引用参数时,左值入参会被特殊处理。
  3. 推导传值参数时,constvolatile 入参会被当做非 const volatile 变量处理(因为对象在传递时已经被复制)。
  4. 在模板类型推导中,数组或函数入参会退化为指针,除非模板中使用引用参数。
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值