c++11 条款1:理解模板类型推导

来源:互联网 发布:网站编程 编辑:程序博客网 时间:2024/06/07 17:22

前言

c++98有单独一套类型推导规则:适用于函数模板。c++11修改了这套规则并且增加了两个,一个是auto,一个是decltype。c++14扩展了auto和decltype使用的场景。随着类型推导在应用程序中的使用逐步增加,你可以从那些明显或冗余的类型拼写中解放出来。这样使得c++软件更具有适配性,因为在源代码中某一位置改变类型会自动通过类型推导升级到另一个位置。然而,有时候会生成很难理解的代码,因为编译器产生的类型推导可能没有你希望的那么明显。

如果没有一个全面的对类型推导如何操作的理解,高效的使用现代 c++编程几乎不可能。有很多类型推导产生的场景:调用模板函数,大多数使用auto的场景下,在decltype表达式下,另外就是c++14中神秘的decltype(auto)构造使用时。

本章将描述所有c++开发者都需要掌握的类型推导的内容。本章将解释模板类型推导如何工作,auto如何构建,decltype如何工作。甚至也解释了如何迫使编译器将类型推导的结果可见化,这样你就可以确保编译器进行的类型推导是你所期望的。


条款1:理解模板类型推导

当一个复杂系统的用户忽略其如何工作时,他们依然会对其所做的工作感兴趣,也就是很多关于系统的设计。如果按照这样的标准,模板类型推导算得上c++一个巨大的成功。数百万的c++程序员把参数传给模板函数,获得满意的结果,他们中的很多人甚至给不出传给那些函数的类型是如何推导的最模糊的解释。

如果这些人中有你,那么我有一个好消息和坏消息。好消息是模板的类型推导是现代c++中引人入胜的特征之一--auto的基础。假如你喜欢c++98中的模板类型推导,那你也会喜欢c++11用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的类型推导和传递给函数的参数类型一样,就是说T就是expr的类型。在上面例子中,情况就是x是个int,于是T被推导为int。但情况并不总是这样,T的类型推导并不仅依赖于expr的类型,还会依赖于ParamType的形式。有3种情况:

1.ParamType是一个指针或引用 ,但不是统一引用(universal reference)(统一引用将在条款24中描述,此时你只要知道,它是左值或右值引用都不同)。

2.ParamType是个统一引用。

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

因此我们需要验证3种类型推导的场景,每一种都基于我们通用的模板形式和调用:

template<typename T>
void f(ParamType param);
f(expr);                   // deduce T and ParamType from expr


场景1:ParamType是一个指针或引用 ,但不是统一引用

最简单的情况就是ParamType是一个引用类型或指针类型,但不是一个统一引用。这种情况下类型推导的工作原理如下:

1.如果expr类型是一个引用,忽略引用部分。

2.expr的类型和ParamType模式匹配的部分决定了T。

比如这样的模板,

template<typename T>
void f(T& param); // param is a reference

我们的变量声明是这样的,

int x = 27;            // x is an int

const int cx = x;   // cx is a const int

const int& rx = x; // rx is a reference to x as a const int

各种变量对ParamType和T的推导出的类型如下,

f(x);                      // T is int, param's type is int&
f(cx);                    // T is const int,
                            // param's type is const int&
f(rx);                    // T is const int,
                            // param's type is const int&

在第二和第三个调用中,注意到因为cx和rx是常量值,所以T被推导为const int,于是产生了一个const int&的参数类型,对调用者来说这很重要,当他们传递一个常量对象给一个引用参数时,他们希望对象仍然是不可改变的,也就是说参数是个指向const的引用。这就是为什么我们传递一个常量对象给一个T&参数的模板时是安全的:对象的常量性变成了T推导出的类型的一部分。

在第三个例子中,注意到即使rx的类型是引用,T依然被推导为非引用的。这是因为在类型推导中rx的引用特性被忽略了。

这些例子都展示的是左值引用参数,实际上对右值引用参数,类型推导的工作原理是一样的。当然,右值引用的形参需要传递右值引用实参,但这个限制和类型推导无关。

如果我们把f函数的参数由T&变成const T&,情况会有所改变。cx和rx的常量性仍然受重视,但因为我们现在假设param参数是一个常量引用,所以在推导时const不再作为T的一部分了:

template<typename T>
void f(const T& param);           // param is now a ref-to-const


int x = 27;                 // as before
const int cx = x;        // as before
const int& rx = x;      // as before
f(x);                          // T is int, param's type is const int&
f(cx);                        // T is int, param's type is const int&
f(rx);                         // T is int, param's type is const int&

和以前一样,rx的引用特性在类型推导中被忽略了。

假如param参数是一个指针(或指向常量的指针)而不是引用,事情本质上一样的:

template<typename T>
void f(T* param);              // param is now a pointer
int x = 27;                         // as before
const int *px = &x;            // px is a ptr to x as a const int


f(&x);                                // T is int, param's type is int*
f(px);                                // T is const int,
                                        // param's type is const int*

到此,你也许发现你哈欠连天的打着盹,因为对参数为引用或指针,c++的类型推导规则工作的非常自然,看这些文档非常的无趣,所有的事情都非常显然,正是你所期望的类型推导系统所做的。


场景2:ParamType是个统一引用

模板使用统一引用的话,事情就不是那么显然了。这样的参数被称作右值参数(就是说在函数模板用T做参数时,统一引用的声明参数为T&&),但当左值参数传入时,它们的表现不一样。详细描述在条款24中,但这里有个目录版本:

1.假如expr是个左值,T和ParamType都被推导为左值引用。这里有两个地方不同寻常,第一,这是唯一一种场景下模板类型推导把T推导成一个引用。第二,尽管ParamType是使用了右值引用的语法来声明的,它的推导类型却是一个左值引用。

2.假如expr是个右值,通用规则(就是场景1)会适用。

比如:

template<typename T>
void f(T&& param);              // param is now a universal reference


int x = 27;                            // as before
const int cx = x;                   // as before
const int& rx = x;                 // as before

f(x);                                     // x is lvalue, so T is int&,
                                           // param's type is also int&

f(cx);                                  // cx is lvalue, so T is const int&,
                                          // param's type is also const int&

f(rx);                                  // rx is lvalue, so T is const int&,
                                         // param's type is also const int&
f(27);                                 // 27 is rvalue, so T is int,
                                         // param's type is therefore int&&

条款24明确的解释了这些例子如果工作的。这里我们理解的关键点是统一引用参数的类型推导规则不同于左值或右值引用,当统一引用使用时,类型推导有别于左值参数或右值参数。这一点在非统一引用中不会发生。


场景3:ParamType既不是指针也不是引用

当ParamType既不是指针也不是引用时,我们执行的是传值参数:

template<typename T>
void f(T param); // param is now passed by value

这意味着无论传递什么,param都会是个拷贝---一个新的对象。param为一个全新对象的事实促使了怎样从expr推导出T的规则:

1.如前,如果expr的类型是个引用,则引用部分被忽略。

2.假如忽略了引用部分后,expr是一个const,也忽略之。如果expr是个volatile,也忽略之。(volatitle对象不常用,它们仅仅使用在实现设备驱动时。详见条款40)

因此,

int x = 27;               // as before
const int cx = x;      // as before
const int& rx = x;    // as before
f(x);                        // T's and param's types are both int
f(cx);                      // T's and param's types are again both int
f(rx);                       // T's and param's types are still both int

注意,即使cx和rx都表示const值,param也不是常量。这也讲得通,param是个完全独立于cx或rx的对象---是cx和rx的拷贝。cx和rx不能被修改而param可以被修改。这就是为什么expr的常量性(或者volatile性以及其他等等特性)在类型推导时可以被忽略:仅仅因为expr不能被修改并不意味着其拷贝也不能被修改。

认识到const(以及volatile)仅仅在传值时可以被忽略这一点是很重要的。就如我们之前看到的,在引用传递或指针传递时,expr的常量特性在类型推导时被保留了。但是我们考虑下,当expr是一个指向常量对象的常量指针时,expr按照传值方式传递给param:

template<typename T>
void f(T param);               // param is still passed by value


const char* const ptr =    // ptr is const pointer to const object
    "Fun with pointers";


f(ptr);                               // pass arg of type const char * const

这里,星号右边的const声明ptr是个const指针:ptr不能指向别的位置,也不能置空(星号左边的const指明ptr指向的内容,也就是字符串是const的,不能被修改)。当ptr被传递给f,指针按字节拷贝到param,这样指针本身(ptr)会按值传递给param,根据按值传递参数的类型推导规则,ptr的常量性将被忽略,param的类型被推导成const char*,也就是一个指向常量字符串的可以被修改的指针。ptr指向的内容的常量性在类型推导中被保留了,但是ptr自身的常量性被忽略了,因为当拷贝到param时,产生了新的指针。


数组参数

以上覆盖了主要的模板类型推导场景,但还是有个值得了解的特定场景,那就是不同于指针类型的数组类型,即使这两者之间有时可以互相转换。造成这种错觉的原因主要是在很多场景下,一个数组可以退化为一个指向头元素的指针。这个退化使得以下的代码可以编译:

const char name[] = "J. P. Briggs"; // name's type is
                                                        // const char[13]


const char * ptrToName = name;    // array decays to pointer


这里,const char* 类型的ptrToName变量初始化为name,name是一个const char[13]。这两个类型(const char*和const char[13])不相同,但是因为数组到指针的退化规则,代码可以编译。

但如果把数组传递给按值传参的模板呢?会发生什么?

template<typename T>
void f(T param); // template with by-value parameter


f(name); // what types are deduced for T and param?

我们开始先假设没有函数的参数可以使用数组。是的,是的,这的确是合法的,
void myFunc(int param[]);

但是数组的声明会被当作指针声明,这意味着myFunc函数可以同样如下声明:

void myFunc(int* param); // same function as above

数组和指针的相等源自于c++的根是c,它造成了数组和指针类型是相同的这样的错觉。

因为数组参数的声明被当作是指针参数,所以通过传值传递给函数模板的数组会被推导为一个指针类型。这意味着在调用函数模板f中,其参数类型T会被推导成const char* :

f(name); // name is array, but T deduced as const char*

但是有个意外出现了。尽管函数不能声明真正的数组作为参数,但能声明数组的引用作为参数!所以我们如果把模板f改成接收引用,

template<typename T>
void f(T& param); // template with by-reference parameter

然后我们再传递一个数组给它,

f(name); // pass array to f

T的类型推导就会是一个实际的数组类型!这个类型会包含数组的大小,所以在这个例子里,T会被推导成const char[13],f的参数的类型(这个数组的引用)就是const char(&)[13]。语法很变态,但理解它会使你比那些不了解的人增分不少。

有趣的是,声明数组引用的能力可以创建一个模板,推导其数组包含元素的个数:

// return size of an array as a compile-time constant. (The
// array parameter has no name, because we care only about
// the number of elements it contains.)
template<typename T, std::size_t N>                            // see info
constexpr std::size_t arraySize(T (&)[N]) noexcept       // below on
{                                                                                     // constexpr

    return N;                                                                    // and
}                                                                                     // noexcept

条款15解释了这个函数的constexpr会使得其结果在编译中可用(?)这样,通过另一个数组(其大小从 花括号初始化中计算求得)声明一个同样元素数组成为可能:

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


int mappedVals[arraySize(keyVals)];    // so does
                                                              // mappedVals

当然,作为现代c++开发者,你应该更倾向使用std::array尔不是内建数组:

std::array<int, arraySize(keyVals)> mappedVals;   // mappedVals'
                                                                                // size is 7

至于arraySize被声明为noexcept,那是帮助编译器产生更好的代码。详见条款14.


函数参数

C++中数组不是唯一退化成指针的东西,函数类型也可以退化成函数指针,我们之前讨论的所有关于数组的类型推导同样适用于函数的类型推导以及函数到函数指针的退化。如下:

void someFunc(int, double);    // someFunc is a function;
                                                // type is void(int, double)

template<typename T>
  void f1(T param);                  // in f1, param passed by value


template<typename T>
  void f2(T& param);               // in f2, param passed by ref


f1(someFunc);                      // param deduced as ptr-to-func;
                                             // type is void (*)(int, double)

f2(someFunc);                     // param deduced as ref-to-func;
                                            // type is void (&)(int, double)


实际上两者几乎没有区别,一旦你了解了从数组到指针的退化,你也会理解函数到指针的退化。

到此你了解了:模板类型推导和auto相关规则,我之前描述过他们很相似,大部分情况下一样。当处理统一引用的类型推导时相关左值处理会有点特殊的地方,然而,数组和函数的指针退化规则更加乱一些。有时你只是简单的想问你的编译器,“告诉我你推导出什么类型”,此刻,你最好到条款4,那里有怎么样使得编译器如此做的方法。

                                            应该记住的

1. 在进行模板的类型推导时,传入参数如果是引用,会被当作非引用,即忽略掉引用部分。

2. 对统一引用参数进行类型推导时,左值参数会获得特殊处理。

3. 按值传递的参数进行类型推导时,const或者volatile参数会被处理成非const或非volatile。

4. 在模板类型推导时,数组和函数参数会退化成指针,除非它们被用做初始化引用。



2 0
原创粉丝点击