现代C++之理解模板类型推断(template type deduction)

简介: 现代C++之理解模板类型推断(template type deduction)目录ParamType是指针或者引用类型ParamType是一个Universal ReferenceParamType既不是指针也不是引用数组参数函数参数要点总结正文理解模板类型推断(template type deduction)我们往往不能理解一个复杂的系统是如何运作的,但是却知道这个系统能够做什么。

现代C++之理解模板类型推断(template type deduction)
目录

ParamType是指针或者引用类型
ParamType是一个Universal Reference
ParamType既不是指针也不是引用
数组参数
函数参数
要点总结

正文

理解模板类型推断(template type deduction)
我们往往不能理解一个复杂的系统是如何运作的,但是却知道这个系统能够做什么。C++的模板类型推断便是如此,把参数传递到模板函数往往能让程序员得到满意的结果,但是却不能够比较清晰的描述其中的推断过程。模板类型推断是现代C++中被广泛使用的关键字auto的基础。当在auto上下文中使用模板类型推断的时候,它不会像应用在模板中那么直观,所以理解模板类型推断是如何在auto中运作的就很重要了。

下面将详细讨论。看下面的伪代码:

template
void f(ParamType param);
通过下面的代码调用:

f(expr); //call f with some expression
在编译过程中编译器会使用expr推断两种类型:一个T的类型,一个是ParamType。而这两种类型往往是不一样的,因为ParamType通常会包含修饰符,比如const或者引用。如果一个模板被声明为下面这个样子:

template
void f(const T& param);//ParamType is const T&
通过如下代码调用:

int x = 0;
f(x); //call f with an int
T会被推断成int,但是 ParamType会被推断成const int&。

我们很自然的会认为对T的推断和传递到函数的参数的推断是相同的,上面的例子就是这样的,参数x的类型为int,T也被推断成了int类型。但是往往情况不是这样子的。对T的类型推断不仅仅依赖参数expr的类型,也依赖ParamType的形式。

有三种情况:

ParamType是指针或者引用类型,但不是universal reference(这个类型在以后的篇章中会讲到,现在只需要明白,这种类型不同于左值引用和右值引用即可。)
ParamType是universal reference。
ParamType即非指针也非引用。
下面将分别进行举例,每个例子都从下面的模板声明和函数调用伪代码演变而来:

template
void f(ParamType param);
f(expr);
回到顶部
ParamType是指针或者引用类型
这种情况下的类型推断会是下面这个样子:

如果expr的类型是引用,忽略引用部分。
然后将expr的类型同ParamType进行模式匹配来最终决定T。
看下面的例子:

template
void f(T &param);
声明如下变量:

int x = 27; //x 为int
const int cx = x;//cx为const int
const int& rx = x;//rx为指向const int的引用
对param和T的推断如下:

f(x); //T被推断为int,param的类型被推断为 int &
f(cx);//T被推断为const int,param的类型被推断为const int &
f(rx);//T被推断为const int(这里的引用会忽略),param的类型被推断为const int &
第二个和第三个函数调用中,cx和rx传递的是const值,因此T被推断成const int,产生的参数类型就是const int &,当你向一个引用参数传递一个const对象的时候,你不会希望这个值被修改,因此参数应该会被推断成为指向const的引用。模板类型推断也是这么做的,在推断类型T的时候const会变为类型的一部分。

第三个例子中,rx的类型是引用类型,T却被推断为非引用类型。因为类型推断过程中rx的引用类型会被忽略。

上面的例子只是说明了左值引用参数,对于右值引用参数同样试用。

如果我们将函数f的参数类型改成cont T&,实参cx和rx的const属性肯定不会变,但是现在我们将参数声明成为指向const的引用了,因此没有必要将const推断成为T的一部分:

template
void f(const T &param);
声明的变量不变:

int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变
对param和T的推断如下:

f(x); //T被推断为int,param的类型被推断为const int &
f(cx);//T被推断为int,param的类型被推断为const int &
f(rx);//T被推断为int(引用同样被忽略) ,param的类型被推断为const int &
如果param是指针或者指向const的指针,本质上同引用的推断过程是相同的。

指针和引用作为模板参数在推断过程中的结果是显而易见的,下面的例子就隐晦一些了。

回到顶部
ParamType是一个Universal Reference
这种类型的参数在声明时形式上同右值引用类似(如果一个函数模板的类型参数为T,将其声明为Universal Reference写成TT&&),但是传递进来的实参如果为左值,结果同右值引用就不太一样了(以后会讲到)。

Universal Reference的模板类型推断将会是下面这个样子:

如果expr是一个左值,T和ParamType都会被推断成左值引用。有点不可思议,首先,这是模板类型推断中唯一将T推断为引用的情况;其次,虽然ParamType的声明使用右值引用语法,但它最终却被推断成左值引用。
如果expr是一个右值,参考上一节(ParamType是指针或者引用类型)。
举个例子:

template
void f(T &&param);

int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变
对param和T的推断如下:

f(x); //x为左值,因此T为int&,ParamType为 int&
f(cx);//cx为左值,因此T为const int&,ParamType也为const int&
f(rx);//rx为左值,因此T为const int&,ParamType也为const int&
f(27);//27为右值,T为int ,ParamType为int&&
这里的关键点是,模板参数为Universal Reference类型的时候,对于左值和右值的推断情况是不一样的。这种情况在模板参数为非Universal Reference类型的时候是不会发生的。

回到顶部
ParamType既不是指针也不是引用
这种情况也就是所谓的按值传递:

template
void f(T param);//按值传递
传递到函数f中的实参值会是原来对象的一份拷贝。这决定了如何从expr中推断T:

同情况一类似,如果expr的类型是引用,忽略引用部分。
如果expr是const的,同样将其忽略。如果是volatile的,同样忽略。
看例子:

int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变
对param和T的推断如下:

f(x); // T为int ParamType为 int
f(cx);//同上
f(rx);//同上
可以看到即使cx和rx为const,param也不是const的。因为param只是cx和rx的一份拷贝,所以不论param的类型如何都不会对原值造成影响。不能修改expr并不意味着不能修改expr的拷贝。

注意只有param是by-value的时候,const或者volatile才会被忽略。我们在前面的例子中说明了,如果参数类型为指向const的引用或者指针,类型推断过程中expr的const属性会被保留。但是看一下下面的情况,如果expr为指向const对象的const指针,而param的类型为by-value,结果会是什么样子的呢:

template
void f(T param);//按值传递

const char * const ptr = "Fun with pointers";
f(ptr);
我们先回忆一下const指针,星号左边的const(离指针最近)表示指针是const的,不能修改指针的指向,星号右边的const表示指针指向的字符串是const的,不能修改字符串的内容。当ptr传递给f的时候,指针本身是按值传递的。因为在by-value参数的类型推断中const属性会被忽略,因此指针的const也就是星号右边的const会被忽略,最后推断出来的参数类型为const char * ptr,也就是可以修改指针指向,不能修改指针所指内容。

回到顶部
数组参数
上面的三种情况涵盖了模板类型推断的大部分情况,但是有另外一种情况不得不说,就是数组。虽然数组和指针有时候看上去是可以互换的,造成这种幻觉的一个主要原因是在许多情况下,数组可以退化为指向第一个数组元素的指针,正是这种退化下面的代码才能编译通过:

const char name[]="HarlanC";//name的类型为const char[8]
const char*ptrToName = name;//数组退化成指针
虽然指针和数组的类型不同,但由于数组退化为指针的规则,上边的代码能够编译通过。

如果将数组传递给带有by-value参数的模板,会发生什么呢?

template
void f(T param);//按值传递
f(name);
将数组作为函数参数的语法是合法的。

void myFunc(int param[]);
但是这里的数组参数会被当做指针参数来处理,也就是说下面的声明和上面的声明是等价的:

void myFunc(int* param); // same function as above
因为数组参数会被当做指针参数来处理,所以将一个数组传递给按值传递的模板函数会被推断为一个指针类型。当调用模板函数f的时候,类型参数T会被推断成const char*:

f(name); // name is array, but T deduced as const char*
虽然函数不能声明一个真正的数组参数(即使这么声明也会被当做指针来处理),但是能够将参数声明为指向数组的引用。我们将模板函数做如下修改:

template
void f(T& param);//按引用传递
传递一个数组实参:

f(name);
这时候会将T推断成一个真正的数组类型。这个类型同时包含了数组的大小,在上面的例子中,T会被推断成const char [8],而f的参数类型为const char (&)[8]。

使用这种声明有一个妙用。我们可以创建一个模板来推断出数组中包含的元素数量:

//在编译期返回数组大小 ,
//注意下面的函数参数是没有名字的
//因为我们只关心数组的元素数量
template
constexpr std::size_t arraySize(T (&)[N]) noexcept
{

return N; 

}
将函数返回值声明成constexpr类型的意味着这个值在编译期就能够得到。这样我们可以在编译期获取一个数组的大小,然后声明另外一个相同大小的数组:

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
int mappedVals[arraySize(keyVals)];
使用std::array更能够体现你是一个现代C++程序员:

std::array mappedVals;
回到顶部
函数参数
数组不是能够退化成指针的唯一类型。函数类型也能够退化为指针,我们所讨论的关于数组的类型推断过程同样适用于函数:

void someFunc(int, double); // someFunc是一个函数,类型为void(int, double)

template
void f1(T param); //passed by value
template
void f2(T& param); // passed by ref
f1(someFunc); // param 被推断为 ptr-to-func void (*)(int, double)
f2(someFunc); // param 被推断为ref-to-func void (&)(int, double)
回到顶部
要点总结
模板类型推断会把引用当做非引用来处理,也就是说会把参数的引用属性忽略掉。
当模板参数类型为universal reference 时,进行类型推断会对左值入参做特殊处理。
当模板类型参数为by-value时,const或者volatile会被当做非const或者非volatile处理。
当模板类型参数为by-value时,入参为函数或者数组时会退化为指针。

作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

相关文章
|
1月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
273 10
|
编译器 C++
【C++】——初识模板
【C++】——初识模板
【C++】——初识模板
|
5月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
205 0
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
158 0
|
8月前
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`<>`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
9月前
|
编译器 C++
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
㉿㉿㉿c++模板的初阶(通俗易懂简化版)㉿㉿㉿
|
9月前
|
安全 C++
【c++】模板详解(2)
本文深入探讨了C++模板的高级特性,包括非类型模板参数、模板特化和模板分离编译。通过具体代码示例,详细讲解了非类型参数的应用场景及其限制,函数模板和类模板的特化方式,以及分离编译时可能出现的链接错误及解决方案。最后总结了模板的优点如提高代码复用性和类型安全,以及缺点如增加编译时间和代码复杂度。通过本文的学习,读者可以进一步加深对C++模板的理解并灵活应用于实际编程中。
142 0
|
9月前
|
存储 安全 算法
深入理解C++模板编程:从基础到进阶
在C++编程中,模板是实现泛型编程的关键工具。模板使得代码能够适用于不同的数据类型,极大地提升了代码复用性、灵活性和可维护性。本文将深入探讨模板编程的基础知识,包括函数模板和类模板的定义、使用、以及它们的实例化和匹配规则。
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
390 4