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

理解模板类型推断(template type deduction)

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

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

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

通过下面的代码调用:

f(expr); //call  f with some expression

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

template<typename T>
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<typename T>
void f(ParamType param);
f(expr);

ParamType是指针或者引用类型

这种情况下的类型推断会是下面这个样子:

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

看下面的例子:

template <typename T>
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 <typename T>
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 <typename T>
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 <typename T>
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 <typename T>
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 <typename T>
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 <typename T>
void f(T& param);//按引用传递

传递一个数组实参:

f(name);

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

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

//在编译期返回数组大小 ,
//注意下面的函数参数是没有名字的
//因为我们只关心数组的元素数量
template<typename T, std::size_t N>
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<int, arraySize(keyVals)> mappedVals;

函数参数

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

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

template<typename T>
void f1(T param); //passed by value
template<typename T>
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时,入参为函数或者数组时会退化为指针。

原文地址:https://www.cnblogs.com/harlanc/p/10565917.html

时间: 2024-08-04 05:39:24

现代C++之理解模板类型推断(template type deduction)的相关文章

[Effective Modern C++] Item 1. Understand template type deduction - 了解模板类型推断

条款一 了解模板类型推断 基本情况 首先定义函数模板和函数调用的形式如下,在编译期间,编译器推断T和ParamType的类型,两者基本不相同,因为ParamType常常包含const.引用等修饰符 template<typename T> void f(ParamType param); // 函数模板形式 f(expr); // 函数调用 存在T的类型即为expr类型的情况,如下T为int templat<typename T> void f(const T& param

《Effective Modern C++》翻译--条款1: 理解模板类型推导

北京2016年1月9日13:47:17 开始第一章的翻译. 第一章名为 类型推断 分为四个条款: 1理解模板类型推导 2理解auto自动类型推导 3理解decltype操作符 4如何对待推导的类型 第一章 类型推导 C++98有一套单一的类型推导的规则用来推导函数模板.C++11轻微的修改了这些规则并且增加了两个推导规则,一个用于auto,一个用于decltype.接着C++14扩展了auto和decltype可以使用的语境.类型推导的普遍应用将程序员从必须拼写那些显然多余的类型中解放了出来,它

现代C++之理解auto类型推断

理解auto类型推断 上一篇帖子中讲述了模板类型推断,我们知道auto的实现原理是基于模板类型推断的,回顾一下模板类型推断: template <typename T> void f(ParamType param); 使用下面的函数调用: f(expr); 我们看到模板类型推断过程涉及到了模板template.函数f以及参数(包括模板参数和函数参数),调用f的时候,编译器会推断T和ParamType的类型.auto的实现和这三个部分是有着对应关系的.当使用auto声明一个变量,auto关键字

类型安全和类型推断

Swift 是一个类型安全(type safe)的语言.类型安全的语言可以让你清楚地知道代码要处理的值的类型.如果你的代码需要一个String,你绝对不可能不小心传进去一个Int. 由于 Swift 是类型安全的,所以它会在编译你的代码时进行类型检查(type checks),并把不匹配的类型标记为错误.这可以让你在开发的时候尽早发现并修复错误. 当你要处理不同类型的值时,类型检查可以帮你避免错误.然而,这并不是说你每次声明常量和变量的时候都需要显式指定类型.如果你没有显式指定类型,Swift

模板与泛型编程——模板实参推断

一.模板实参推断 对于函数模板,编译器利用调用中的函数实参来确定其模板参数.从函数实参来确定模板实参的过程被称为模板实参推断.在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数与给定的函数调用最为匹配. 1.类型转换与模板类型参数 与非模板函数一样,我们在一次调用中传递给函数模板的实参被用来初始化函数的形参.如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则.只有很有限的几种类型转换会自动地应用于这些实参.编译器通常不是对实参进行类型转

C++11新特性:自动类型推断和类型获取

声明:本文是在Alex Allain的文章http://www.cprogramming.com/c++11/c++11-auto-decltype-return-value-after-function.html的基础上写成的. 加入了很多个人的理解,不是翻译. 转载请注明出处 http://blog.csdn.net/srzhz/article/details/7934483 自动类型推断 当编译器能够在一个变量的声明时候就推断出它的类型,那么你就能够用auto关键字来作为他们的类型: [c

C++模板实参推断

1 类型转换与模板实参 1)自动转换的只有:const转换, 数组及函数到指针的转换 注:不同大小相同元素类型是不同的类型 2)相同模板参数名对应的实参类型必须相同 3)不同模板参数名对应的实参类型可以不同,但必须兼容 2 函数模板的返回值问题 函数模板只会对函数参数列表的类型进行推断不会对返回值推断 解决方法: 1) 显示模板参数 注: 显示指定了模板类型参数在类型转换上和普通函数参数一样 template <typename T1, typename T2, typename T3> T1

C++ primer-&gt;16.2 模板实参推断

一.类型转换与模板类型参数 1.如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则.只有很有限的几种类型转换会自动地应用于这些实参. ①.顶层const无论是在形参中还是在实参中,都会被忽略. ②.const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用(或指针)形参. ③.数组或函数指针转换:如果函数形参不是引用类型,则可以将对数组或函数类型的实参应用于正常的指针转换. 如下程序所示: 1 template<typename T> T fobj(

【ThinkingInC++】73、深入理解模板

第五章 深入理解模板 5.1 模板参数 关于bitset bitset就是可以存放二进制的容器. 对于bitset的主要操作有: (constructor) Construct bitset (public member function)    //构造bitset..   格式bitset<长度>  名字 applicable operators Bitset operators (functions)          //可以直接对bitset容器进行二进制操作,如^,|,~,<