item 1:理解template类型的推导

item 1: 理解template类型的推导

一些用户对复杂的系统会忽略它怎么工作,怎么设计的,但是很高兴去知道它完成的一些事。通过这样的方式,c++中的template类型的推导取得了巨大的成功。数以万计的程序员曾传过参数给template函数,并得到了满意的结果。尽管很多那些程序员很难给出比朦胧的描述更多的东西,比如那些被推导的函数是怎么使用类型来推导的。

如果你也是其中的一员,我这有好消息和坏消息给你。好消息是template类型的推导是现代c++最令人惊叹特性之一(auto)的基础。如果你熟悉c++98的template类型推导,那么你也会熟悉c++怎么使用auto来做推导类型。而坏消息是当template类型推导规则应用在auto上时,比起应用在template中时,他们有时候看起来会比较难理解。由于这个原因,我们很有必要完全理解template类型推导的各个方面,因为auto是建立在这个基础上的。这个条款包含了你想知道的东西。

如果你愿意浏览少许伪代码,我们可以把函数模板看成这样子:

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

对于函数的调用看起来是这样的:

f(expr);

在整个编译期间,编译器用expr来推导两个类型:一个是T ,另一个是 
ParamType 。这两个类型常常是不一样的,因为ParamType常常包含一些修饰符。(比如const或引用的限定)。比方说,如果一个template声明成这样:

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

然后我们这样使用它:

int x = 0;
f(x);

T被推导成int,但是ParamType被推导成const int&。

很自然会觉得T类型就是传入函数的实参类型,也就是T 是 expr的类型。 在上面的例子中,x是int,T被推导成int,但是它不总是这样工作的。 对T类型的推导,不仅仅取决于expr ,同时也取决于ParamType。这里有三种情况:

  • ParamType是指针或引用,但不是一个universal引用(universal引用在#24中描述,现在,你要知道的就是,他们存在,并且左引用和右引用是不相同的)
  • ParamType是universal引用。
  • ParamType不是指针也不是引用

因此我们有三种类型的推导情况需要分析。每一个将建立在下面这个template的基础上,并如此调用:

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

f(expr);

情况1:参数是指针或引用,但不是一个universal引用

这种情况是最简单的情况,类型推导将这么工作:

  1. 如果expr的类型是引用,忽略引用的部分。
  2. 然后用expr的类型模式匹配ParamType来决定T

比方说,下面的template:

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

并且我们有这些变量声明:

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

对于各种调用,paramT的类型推导是下面这用的:

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

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

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

注意在第二个和第三个函数调用中,因为cx和rx是const变量,T被推导成cosnt int,因此param类型是const int&。这对调用者是很重要的。当他们传入const引用对象参数,他们希望对象是不可改动的。也就是,参数要是一个reference-to-const。这也是为什么传入一个const对象给使用T&参数的template是安全的:对象的const属性会被推导成T的一部分。

注意在第三个例子中,即使rx的类型是引用,T还是被推导成非引用。这是因为第一点,在推导时,rx的引用属性被忽略了。

这些例子显示了左值引用,但是对右值引用的推导和左值引用是一样的。

如果我们把f的参数从T&改成 const T&,情况将发生小小的变化,但是不会出乎意料。cx和rx的const属性仍然存在,但是因为我们现在假设param是const引用,所以就没有必要把const属性推导到T上去了:

f(x);   //T是int,    param的类型是 cosnt int&

f(cx);  //T是int,    param的类型是 cosnt int&

f(rx);  //T是int,    param的类型是 cosnt int&

同样地,rx的引用属性被忽略了。

如果param是一个指针(或者指向const的指针),情况和引用是一样的:

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

int x = 27;
const int *px = &x;

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

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

现在你可能觉得有些困了,因为对于引用和指针类型,c++的类型推导规则是如此的自然,把他们一个个写出来是如此枯燥,因为所有的事情显而易见,就和你想要的推导系统一样。

情况2:参数是一个universal引用

对于一个采用universal引用参数的template,情况变得复杂。这样的参数大多被声明成右值引用(比如在函数模板采用一个类型T,一个universal引用的声明类型是T&&),但是他们表现的和传入左值参数时不同。完整的情况将在item 24讨论,但是这里给出一些大概内容:

  • 如果expr是一个左值,Tparam会被推导成左值引用,这里有两个不寻常点。第一,这是唯一一种T被推导成引用的情况。第二,尽管ParamType在语法上被声明成一个右值引用,但是他的推导类型是一个左值引用。
  • 如果expr是一个右值,适用情况1的规则

举个例子:

template<typename T>
void f(T&& 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&&

item 24解释了为什么这个例子会这样工作。这里的关键点是universal引用的类型推导规则对左值和右值是不同的。尤其是,当universal引用在使用中,类型推导会根据传入的值是左值还是右值进行区分。non-universal引用永远不会发生这样的情况。

情况3:参数不是指针也不是引用

ParamType不是指针也不是引用时,我们处理传值(by-value)情况:

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

这意味着,param将成为传入参数的一份拷贝,一个全新的对象。

  1. 和以前一样,如果expr的类型是引用,忽略引用属性。
  2. 如果,在忽略expr的引用属性后,expr是const,再忽略const属性。如果是volatile的,同样忽略。(volatile对象不常见,他们通常用来实现设备驱动,详细的情况,在item40中讨论)

因此:

int x = 27;
cosnt 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能不能被改变和param能不能被改变没有联系,这也就是为什么在推导类型的时候,expr的const属性被忽略了。只是因为expr不能被改变不意味着他们的拷贝不能被改变。

很重要的是,你要知道const(和volatile)属性被忽略只适用于传值(by-value)参数。就像我们看到的,在推导类型的时候,传引用(references-to)或传指针(pointers-to)参数的const属性是会保留的。但是我们来考虑一下expr是cosnt指针指向const类型的情况,并且expr是传值(by-value)参数:

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

cosnt char* cosnt ptr = "Fun with pointers";

f(ptr); //T是const char,param的类型是const char*

这里,星号右边的const声明ptr是const的:ptr不能指向别的地方,也不能设为空。(星号左边的const意味着ptr指向的一个---字符串---是const的,因此不能被修改)当ptr传入f,ptr会拷贝一份,赋给param,因此,指针本身(ptr)将以传值(by-value)形式传入。同样的类型推导规则,传值(by-value)参数ptr的const属性将会被忽略,所以*param的类型会被推导成cosnt char,一个指向const字符类型的可以改变指向的指针。

数组参数

之前的那些已经设计到大多数主流的template参数推导了,但是,这里还有一些小部分的情况直的我们去了解。数组类型不同于指针类型,尽管有时候它们看起来可以相互替换。这个错觉主要来自于:在很多时候,一个数组可以退化成(decays)一个指向其第一个元素的指针。这个退化允许代码写成这样:

const char name[] = "J.P.Briggs";

const char* ptrToName = name;

这里,一个const char* 指针ptrToname用const char[13]的数组*name来初始化。这些类型(cosnt char 和 const char[13])不一样,但是由于array-to-point的退化规则,代码可以编译。

但是,当把数组传值(by-value)传入函数时会发生什么:

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

f(name);

我们从函数没有数组参数开始,是的,是的,这样的语法是合法的:

void myFunc(int param[]);

但是数组的声明会被当成指针的声明,这意味着myFunc能等价地声明成:

void myFunc(int* param);

这种对于数组和指针参数的等价情况是从C那边来的,并且这增加了数组和指针相同的错觉。

因为数组参数声明被对待成指针参数,通过传值(by-value)传入template函数的数组被推导成指针类型。这意味着上面的参数T被推导成const char*:

f(name);    //name是数组,但是T被推导成cosnt char*

但是,现在有个问题,尽管函数不能声明真正的数组参数,但是他们可以声明指向数组的引用参数!所以,如果我们修改函数为传引用(by-references):

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

并且我们传入一个数组:

f(name);

T的类型推导将是真正的数组类型!这个类型包含了数组的大小,所以在这个例子中,T被推导成cosnt char[13],并且param的类型(一个引用指向这个数组)是cosnt char(&)[13]。是的,这语法看起来有毒,但是知道这个规则会让你了解到别人很少注意的点。

有趣的是,声明指向数组的引用使我们能创造一个template来推导数组的大小:

//在编译期返回数组大小(数组参数没有名字,因为我们仅仅为了知道数组的大小)
template<typename T, std::size_t N>
cosntexpr std::size_t arraySize(T(&)[N]) noexcept
{
    return N;
}

item15中解释声明这样的constexpr的函数可以让结果在编译期返回。这使我们可以用一个数组的大小做为另一个新数组的大小来初始化新数组:

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35};

int mappedVals[arraySize(keyVals)];

当然了,作为一个现代C++开发者,你可能更喜欢一个std::array来建立一个数组:

std::array<int, arraySize(keyVals)> mappedValsl;

对于arraySize被声明为noexcept,这能帮助编译器产生更好的代码。想知道细节,可以看item 14。

函数参数

在C++中,数组不是唯一的退化成指针的情况,函数类型也会退化成函数指针,并且所有的我们对数组的推导的讨论适用于函数类型推导:

void someFunc(int, double);

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

template<typename T>
void f2(T& param);

f1(someFunc);   //T是void (*)(int, double)

f2(someFunc);   //T是 void ()(int, double)

这看起来几乎没有任何不同,但是,如果你已经知道了array-to-pointer的退化,你也会同样知道function-to-pointer的退化。

现在你明白了吧:函数类型推导的规则,我在开始就说他们相当简单,并且大多数情况下是这样的。对于universal引用,左值当推导的类型时需要特别对待,这使我们有点晕晕的,然而退化成指针(decay-to-pointer)的规则使得我们更加晕了。有时,你简单地想抓住你的编译器和并渴望:“告诉我你推导的类型是什么”。当发生这样的事时,转到item 4,因为这条款专注于欺骗编译器去做你想它做的事。

            你要记住的事
  • 在template类型推导的时候,references类型的参数被当成non-references。也就是说引用属性会被忽略。
  • 当推导universal类型的引用参数时,左值参数被特殊对待。
  • 当推导传值(by-value)类型参数时,cosnt 和/或 volatile 参数被当成 non-const 和 non-volatile。
  • 当推导类型是数组或函数时会退化成指针,除非形参是引用。
时间: 2024-07-30 10:19:23

item 1:理解template类型的推导的相关文章

《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++之理解模板类型推断(template type deduction)

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

Effective C++ Item 48 认识 template 元编程

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 经验:Template metaprogramming (TMP, 模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率 示例1: template<typename IterT, typename DistT> void advance(IterT &iter, DistT d){ if(typeid(typename std::iterator_t

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

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

Java基础 -- 深入理解Java类型信息(Class对象)与反射机制

一 RTTI概念 认识Claa对象之前,先来了解一个概念,RTTI(Run-Time Type Identification)运行时类型识别,对于这个词一直是 C++ 中的概念,至于Java中出现RTTI的说法则是源于<Thinking in Java>一书,其作用是在运行时识别一个对象的类型和类的信息,这里分两种: 传统的”RTTI”:它假定我们在编译期已知道了所有类型(在没有反射机制创建和使用类对象时,一般都是编译期已确定其类型,如new对象时该类必须已定义好): 反射机制,它允许我们在运

《Effective Modern C++》读书笔记 Item 2 auto的类型推导

注意: 还要学习一个 ↑↑↑↑ 这样的方框里的片段完全不来自于原书,而是我自己的理解. Item 2 Understand auto type deduction - auto类型推导 在C++11之前,auto 关键字一直是用于声明自动储存类型的变量时使用的,基本上没有什么实际作用,地位和 export 关键字(用于向编译单元之外导出模板,也在C++11中被取消)类似. 在C++11中,auto 终于不再废材,终于具备了类似C#中 var 关键字的效果,可以自动推导出变量的类型,可以少打几个字

[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 c++ Item 49 理解new-handler的行为

1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨论推迟到本条款结束的时候. 1.1 调用set_new_handler来指定全局new-handler 在operator new由于不能满足内存分配要求而抛出异常之前,它会调用一个客户指定的叫做new-handler的错误处理函数.(这也不是完全正确的.Operator new的真正行为更加复杂.

读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)

最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热情和追求还是不减,应该是感动了周公吧,梦境从此处开始,大师入场来给我安慰了... 11点躺在床上了,脑子里总结一下最近的工作:最近的开发用到inline函数比较多,众所周知,inline的使用是为了提高程序性能,可结果却总不尽如人意,这个捉急啊,嗯?怎么突然到了山脚下,周边树木林立,郁郁葱葱,鸟儿委婉啼叫