理解模板类型推导
- 模板类型推导是 Modern C++ auto 类型的基础
- 模板类型推导规则也同样应用于auto,只是不如应用于模板类型那么直观
template
void f(ParamType param);
// 以expr表达式为参数,调用f (模板)函数
f(expr);
// 编译器使用expr推断两种类型:一种用于T,一种用于ParamType。
// 这两种类型是不同的,因为ParamType通常包含装饰,例如const或引用限定符,
// 即 paraType为 [const] T [&]
template
void f(const T& param); // ParamType 为 const T&
int x = 0;
f(x); // 以 int 实参调用 f函数
// T 被推导成int,
// ParamType 被推导为 const int&.
T 的类型推导不仅取决于 expr 的类型, 还取决于 ParamType 的形式。 有以下三种情况:
- ParamType 是指针或引用类型, 但不是万能引用(universal reference)。
- ParamType 是万能引用.
- ParamType 既不是指针,也不是引用.
Case 1: ParamType 是指针或引用类型, 但不是万能引用(universal reference)
在这种情况下,类型推导的工作原理如下:
- 如果 expr 的类型是引用,则忽略引用部分。
- 然后将expr的类型与ParamType模式匹配,以确定T。
- 从 expr 的类型推断出 paramtype 的类型,继而推断出T的类型。
template
void f(T& param); // param 为 引用类型
int x = 27; // x 为 int 类型
const int cx = x; // cx 为 const int 类型
const int& rx = x; // rx 是 x 的引用,为 const int 类型
f(x); // param 的类型为 int&, T 的类型为 int.
f(cx); // param 的类型为const int&, T 的类型为 const int.
f(rx); // param 的类型为const int&, T 的类型为 const int.
当调用者将const对象传递给引用参数时,他们希望该传递的对象保持不可修改的状态,即该引用参数是对const的引用。 这就是为什么将const对象传递给带有T&参数的模板是安全的:对象的 const 修饰是T推导类型的一部分。
template
void f(const T& param); //param 为 const 引用类型
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // param 的类型为 const int&, T 的类型为 int.
f(cx);// param 的类型为 const int&, T 的类型为 int.
f(rx); // param 的类型为 const int&, T 的类型为 int.
// 模板参数为指针的工作原理与引用相同
template
void f(T* param); // param 的类型为指针
int x = 27;
const int *px = &x; // px 是 &x 的 const 引用,为 const int* 类型
f(&x);// param 的类型为 int*, T 的类型为 int.
f(px); // param 的类型为 const int*, T 的类型为 const int.
Case 2:ParamType 是万能引用
在这种情况下,类型推导的工作原理如下:
- 如果 expr 是一个左值,则 T 和 ParamType 都是左值引用。这是模板类型推导的唯一情况,T 被推导为引用。 尽管 Param 是使用右值引用的语法声明的,但其推导类型是左值引用。
- 如果 expr 是右值,则适用 case 1 规则。
请注意,在使用万能引用时,类型推导区分左值参数和右值参数。 对于非万能引用(即,普通左值引用,参考 case 1),永远不会发生这种情况。
template
void f(T&& param); // param 为万能引用类型 universal reference
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&&
Case 3:ParamType 既不是指针,也不是引用
实参的const属性,reference属性都不会被考虑,因为按值传递。
template
void f(T param); //param 按值传递
// 这意味着 para 将是传入的任何内容的副本,一个全新的对象。
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
请注意,即使 cx 和 rx 是 const 值,param 也不是const。 这是可以理解的。 param 是完全独立于 cx 和 rx 的对象,是 cx 或 rx 的副本。 无法修改cx和rx的事实并没有说明 param 是否可以修改。 这就是为什么在推导 param 类型时会忽略 expr 的const 属性(和易变性,如果有的话)的原因:仅仅因为不能修改expr,并不意味着就expr的副本不可以被修改。
但是,references-to- or pointers-to-const,
在类型推导过程中, expr 的 const 会被保留, 参考 Case 1。
- 如果 expr 是引用类型,则忽略引用部分。
- 如果在忽略 expr 的引用性之后,expr 拥有 const 属性 ,则也请忽略 const 。 如果其也拥有volatile属性,也可以忽略volatile属性。 (volatile对象很少见。它们通常仅用于实现设备驱动程序。有关详细信息,请参阅item 40。)
template
void f(T param);//param 按值传递
const char* const ptr = "Fun with pointers"; // ptr 是指向 const 对象的 const 指针
f(ptr); // 将指向 const 对象的 const 指针作为实参传入。
// T 和 param 的类型都是 const char*, 指针的 const 属性被忽略。
根据按值参数的类型推导规则,将忽略 ptr 的 const,而为param推导的类型将为 const char *,即指向const字符串的可修改指针。 ptr 指向的const 在类型推导过程中会保留,但是ptr本身的 const 在复制创建新的指针param时将被忽略。
数组参数
数组类型与指针类型不同,即使它们有时似乎可以互换。在许多情况下,数组会转化为指向其第一个元素的指针。 这种转化使诸如此类的代码得以编译:
const char name[] = "J. P. Briggs"; // name 的类型为 const char[13]
const char * ptrToName = name; // 数组转化成指针, ptrToName 的类型为 const char*
const char* 类型的指针变量 ptrToName 正在使用 name 初始化,name 的类型为 const char [13]。 这些类型(const char *和const char [13])不同,但是由于数组到指针的转化规则,代码得以编译。
数组和指针参数的这种等效性是从C ++的C根源衍生而来的,即数组和指针类型相同。
void myFunc(int param[]);
// 数组声明被视为指针声明,这意味着可以等效地声明myFunc,如下所示:
void myFunc(int* param);
// 将数组传递给带有按值参数的模板
const char name[] = "J. P. Briggs"; // name 的类型为 const char[13]
const char * ptrToName = name; // 数组转化成指针, ptrToName 的类型为 const char*
template
void f(T param); // 按值传递的模板
f(name);
// T 和 param 的类型是什么?
// name 是数组类型, 但是 T 被推导为 const char*
//由于将数组参数声明视为指针参数,因此将按值传递给模板函数的数组的类型推导为指针类型
尽管函数无法声明真正的数组形参,但它们可以声明数组引用的形参!
template
void f(T& param); // 引用参数的模板函数
f(name); // 将数组传递给 f函数
// T 推导的类型是数组的实际类型! 该类型包括数组的大小,因此在此示例中, T 推导为const char [13],而 param 的类型为const char(&)[13]。
通过声明对数组的引用,可以创建一个模板,该模板可以推断出数组包含的元素数量:
这个得提前了解 item 15 constexpr 的知识点。
// 返回数组的大小,作为编译时常量。
// 数组参数没有名称,因为我们只关心它包含的元素数,即忽略了param形参。
template
constexpr std::size_t arraySize(T (&)[N]/*param*/) noexcept
{
return N;
}
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals 有7个元素
int mappedVals[arraySize(keyVals)]; //mappedVals 也有7个元素
std::array mappedVals; //mappedVals' size 为 7
按值传递时,数组会退化为指针,涉及数组传递时,最好使用按引用传递
#include
using namespace std;
template
void PrintArray(T(&arr)[size])
{
for (int i = 0; i < size; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
int main()
{
int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
PrintArray(arr);
std::cout << "Hello World!\n";
}
函数参数
数组并不是C++中唯一可以转化成指针的类型。函数类型可以转化为为函数指针,而我们讨论的关于数组类型演绎的所有内容都适用于函数的类型演绎以及它们转化为函数指针。因此:
void someFunc(int, double); // someFunc 为函数类型; 类型为 void(int, double)
template
void f1(T param); // 按值传递
template
void f2(T& param); // 按引用传递
f1(someFunc); // param 被推导为 ptr-to-func; 类型为 void (*)(int, double)
f2(someFunc); // param 被推导为 ref-to-func; 类型为 void (&)(int, double)
//f1,f2 rarely makes any differentce in practice
总结:
在模板类型推导期间,作为引用的参数将被视为非引用,即它们的引用性将被忽略。
在推导通用引用参数的类型时,左值参数会得到特殊处理。
在推导按值参数的类型时,const或volatile参数被视为非const和volatile。
在模板类型推导期间,除非将数组或函数名称的参数用于初始化引用,否则它们会转化为指针。