Effective Modern C++:01类型推导

C++的官方钦定版本,都是以ISO标准被接受的年份命名,分别是C++98,C++03,C++11,C++14,C++17,C++20等。C++11及其后续版本统称为Modern C++。

C++11之前,仅有一套类型推导规则,也就是函数模板的推导。C++11之后,又增加了了auto和decltype的推导规则。模板推导规则是auto的基础。

首先需要介绍顶层const和底层const 的概念:指针本身是不是常量以及指针所指的对象是不是一个常量,这是两个相互独立的问题。顶层const(top-level const)表示指针本身是个常量,而底层const(low-level const)表示所指的对象是一个常量。

更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用;底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const。

int i = ;
int *const p1 = &i; //不能改变p1的值,这是一个顶层const
const int ci = ; //不能改变ci的值,这是一个顶层const
const int *p2 = &ci; //允许改变p2的值,这是一个底层const
const int *const p3 = p2; //靠右的const是顶层const,靠左的是底层const
const int &r = ci; //用于声明引用的const都是底层const

 

 

01:模板推导规则

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

在编译期间,编译器通过expr推导T和ParamType的类型。直觉上,T的类型总是与expr一致,然而实际上,T的类型推到结果,不仅仅依赖于expr的类型,还取决于ParamType的形式。

下面的示例代码,都是gcc version 5.4.0 20160609下验证,使用” __PRETTY_FUNCTION__”宏打印函数模板的具体类型。如果是vs,则可以使用”__FUNCSIG__”宏:

template<typename T>
void f(T &param){
printf("f: %s\n", __PRETTY_FUNCTION__);
}

1:ParamType是个指针或引用,但不是万能引用

若expr是引用(左值引用或右值引用),则先将引用部分忽略;然后,对expr的类型和ParamType的类型进行模式匹配,来决定T的类型。

template<typename T>
void f(T &param); int x = ;
int &rx = x;
const int &crx = x;
f(x); // T is int, param's type is int&
f(rx); // T is int, param's type is int&
f(crx); // T is const int, param's type is const int& const int y = ;
f(y); // T is const int, param's type is const int& int &&rr = ;
f(rr); // T is int, param's type is int& const int &&crr = ;
f(crr); // T is const int, param's type is const int& int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
f(p); // T is int*, param's type is int*&
f(cp); // T is const int*, param's type is const int*&
f(ccp); // T is const char* const, param's type is const char* const &

由于crx和y为const,所以T的类型推到为const int。这也就是为什么向T&类型的模板传入const对象是安全的,因为该对象的常量性会成为T类型推到结果的组成部分。

如果模板函数形参为const T &param,则推导的本质不变,但是结果却略有不同:

template<typename T>
void f(const T& param); // param is now a ref-to-const int x = ;
int &rx = x;
const int &crx = x;
f(x); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&
f(crx); // T is int, param's type is const int& const int y = ;
f(y); // T is int, param's type is const int& int &&rr = ;
f(rr); // T is int, param's type is const int& const int &&crr = ;
f(crr); // T is int, param's type is const int& int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
f(p); // T is int*, param's type is int* const &
f(cp); // T is const int*, param's type is const int* const &
f(ccp); // T is const char*, param's type is const char* const & f(); // T is int, param's type is const int &

注意,模板形参中声明引用的const都是底层const,表示引用的对象为const,而这个const匹配模板实参时匹配的是顶层const。因此对于指针cp而言,它是个指向const int的non-const指针,匹配到模板后,T就是const int*,而param是const int* const &;对于ccp而言,它是个指向const char的const指针,因为param中的const匹配到了其顶层const,所以,T就推导为const char*,而param推导为const char* const &。

上面的例子都是左值引用形参,而右值引用形参的类型推导方式完全相同,当然,传给右值引用形参的,只能是右值,这样的示例在下一条规则中展示。

如果param是个指针,本质上是相同的:

template<typename T>
void f(T *param) int x = ;
int *p = &x;
const int *cip = &x;
int const *cip2 = &x;
int * const icp = &x;
const char * const ccp = "abcd";
f(p); // T is int, param's type is int*
f(cip); // T is const int, param's type is const int*
f(cip2); // T is const int, param's type is const int*
f(icp); // T is int, param's type is int*
f(ccp); // T is const char, param's type is const char*

cip和cip2的类型是一样的,都是指向const int的指针,因此T推导为const int,而param就是const int*;需要注意的是icp,它是个指向int的const指针,推导T得到的结果是个int,param是int*,这里可以视为将指针本身的值进行推导,想一下,顶层const指针是可以赋值给non-const指针的,因为实际上就是赋值指针本身的值;

如果param是const T*:

template<typename T>
void f(const T *param) int x = ;
int *p = &x;
const int *cip = &x;
int const *cip2 = &x;
int * const icp = &x;
const char * const ccp = "abcd";
f(p); // T is int, param's type is const int*
f(cip); // T is int, param's type is const int*
f(cip2); // T is int, param's type is const int*
f(icp); // T is int, param's type is const int*
f(ccp); // T is char, param's type is const char*

这里param中的const是底层const,表示指针指向的对象为const,所以结果如上。

2:ParamType是个万能引用

万能引用是Scott Meyers创造的概念,实际上它就是在C++标准中所谓的转发引用(forward reference)。而且这个概念只出现在函数模板推导和非brace-enclosed initializer list的auto推导中。

https://en.cppreference.com/w/cpp/language/reference

https://blog.petrzemek.net/2016/09/17/universal-vs-forwarding-references-in-cpp/

https://*.com/questions/39552272/is-there-a-difference-between-universal-references-and-forwarding-references

万能引用形参的声明方式类似于右值引用:T&&。规则是:如果expr是个左值,T和ParamType都会被推导为左值引用,这是在模板类型推导中,T被推导为引用的唯一情形;如果expr是个右值,此时ParamType就是个右值引用,则按照规则1进行推导:

template<typename T>
void f(T &&param) int x = ;
int &rx = x;
const int &crx = x;
f(x); // T is int&, param's type is int&
f(rx); // T is int&, param's type is int&
f(crx); // T is const int&, param's type is const int& const int y = ;
f(y); // T is const int&, param's type is const int& int &&rr = ;
f(rr); // T is int&, param's type is int& const int &&crr = ;
f(crr); // T is const int&, param's type is const int& int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
f(p); // T is int*&, param's type is int*&
f(cp); // T is const int*&, param's type is const int*&
f(ccp);// T is const char* const &, param's type is const char* const & f(std::move(x)); // T is int, param's type is int&&
f(std::move(y)); // T is const int, param's type is const int&&
f(std::move(rr)); // T is int, param's type is int&&
f(std::move(p)); // T is int*, param's type is int *&&
f(); // T is int, param's type is int &&

rr和crr尽管是右值引用,但它们本身却是左值,因此,T和param推导为左值引用;最后一组使用std::move以及常量27,传递给f的都是右值,因此,T根据规则1进行匹配,最终推导为非引用,param推导为右值引用;

万能引用的形式只能是T&&(an rvalue reference to a cv-unqualified template parameter(so-called forwarding reference)),一旦写成const T&&,这就是个右值引用了。因此这种情况下,上面示例中的左值均绑定失败,报编译错误:cannot bind ‘XXX’ lvalue to XXX&&。只有使用move的表达式,以及常量才能绑定成功:

f(std::move(x)); //T is int, param's type is const int &&
f(std::move(y)); //T is int, param's type is const int &&
f(std::move(rr)); //T is int, param's type is const int &&
f(std::move(p)); //T is int*, param's type is int* const &&
f(); //T is int, param's type is const int &&

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

如果expr具有引用类型,则忽略其引用部分;忽略引用之后,如果expr是个const对象,也忽略,若是个volatile对象,也忽略;

template<typename T>
void f(T param) int x = ;
int &rx = x;
const int &crx = x;
f(x); // T is int, param's type is int
f(rx); // T is int, param's type is int
f(crx); // T is int, param's type is int const int y = ;
f(y); // T is int, param's type is int int &&rr = ;
f(rr); // T is int, param's type is int const int &&crr = ;
f(crr); // T is int, param's type is int int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
f(p); // T is int*, param's type is int*
f(cp); // T is const int*, param's type is const int*
f(ccp); // T is const char*, param's type is const char* f(); // T is int, param's type is int

因为ParamType既不是指针也不是引用,因此就是按值传递,这意味着无论传入什么,param都是它的一个副本,也就是一个全新的对象。const变量可以用non-const进行初始化,non-const变量也可以通过const变量进行初始化,与const与否无关。因为都是复制到副本;volatile也类似。

需要注意的是:ccp是个指向const对象的const指针,根据规则,这里忽略的是顶层const,而非底层const,也就是说,param的类型会被推导为const char *。

如果模板函数形参为const T param,则结果是:

template<typename T>
void f(const T param) int x = ;
int &rx = x;
const int &crx = x;
f(x); // T is int, param's type is const int
f(rx); // T is int, param's type is const int
f(crx); // T is int, param's type is const int const int y = ;
f(y); // T is int, param's type is const int int &&rr = ;
f(rr); // T is int, param's type is const int const int &&crr = ;
f(crr); // T is int, param's type is const int int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
f(p); // T is int*, param's type is int* const
f(cp); // T is const int*, param's type is const int* const
f(ccp); // T is const char*, param's type is const char* const f(); // T is int, param's type is const int

注意,模板形参中的const是顶层const,因此ccp的情况,T推导为const char*,而param推导为const char* const。

4:数组实参

以上就是模板类型推导的主流情况,不过还有一些边缘情况,比如数组实参。在很多语境下,数组会退化成指向数组元素首元素的指针。因此,当一个数组传递给持有按值形参的模板时:

template<typename T>
void f(T param); const char name[] = "J. P. Briggs";
f(name) // T is const char*, param's type is const char*

数组是不能按值传递的,或者说,当声明下面这种形式的函数时:

void myFunc(int param[]);

实际上,该函数的原型是:

void myFunc(int* param);

针对持有按值形参的模板而言,name数组会退化成const char*,也就是一个指针。因此,T的类型就是const char *。

在C++中,函数的形参可以是数组的引用,因此:

template<typename T>
void f(T& param); f(name); // T is const char (&)[13], param's type is const char (&)[13]

这种情况下,T就会被推导为实际的数组类型:const char[13],这个类型中包含了数组尺寸。f的形参,则是const char (&)[13]。

利用声明数组引用形参,可以创造一个模板,用于推导出数组的元素个数:

//以编译期常量形式返回数组尺寸
//该模板的数组形参没有名字,因为我们只关心数组个数
template<typename T, std::size_t N> // see info
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
} int keyVals[] = { , , , , , , }; // keyVals has 7 elements
int mappedVals[arraySize(keyVals)]; // mappedVals has 7 elements too

constexpr使函数的返回值在编译期可用,其作用后续条款会讲。

5:函数实参

类似于数组,函数类型也会退化为函数指针,并且针对数组类型推导的一切结论,也适用于函数。

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 type is void (*)(int, double)
f2(someFunc); // param type is void (&)(int, double)

02auto类型推导

有时需要把表达式的值赋给变量,这就要求声明变量时清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。为了解决这个问题,C++ 11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式的类型。auto让编译器通过初始值来推算变量的类型。因此,auto定义的变量必须有初始值。

使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,因此该语句中所有变量的初始基本数据类型必须都一样:

auto i = , *p = &i; //正确,i是int,p是int*
auto sz = , pi = 3.14; //错误:sz和pi的类型不一样

在一条语句中定义多个变量,符号&和*只从属于某个声明符,而非基本数据类型的一部分。

关于auto的类型推导,除了一个特殊情况外,函数模板的类型推导规则就是auto类型推导规则。在函数模板的推导规则中:

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

编译器根据expr推导T和ParamType。而在auto类型推导中:

auto x = ;
const auto cx = x;
const auto& rx = x;

auto就对应于函数模板中的T,变量的类型饰词对应ParamType,而=右边的表达式,就对应于expr。

对应于模板推导规则中的几种情况,auto推导规则也按这几种情况讨论。使用decltype以及下面的函数,可以查看auto推导的类型结果:

template <class T>
std::string type_name() {
typedef typename std::remove_reference<T>::type TR;
std::unique_ptr<char, void(*)(void*)> own
(
#ifndef _MSC_VER
abi::__cxa_demangle(typeid(TR).name(), nullptr,
nullptr, nullptr),
#else
nullptr,
#endif
std::free
);
std::string r = own != nullptr ? own.get() : typeid(TR).name();
if (std::is_const<TR>::value)
r += " const";
if (std::is_volatile<TR>::value)
r += " volatile";
if (std::is_lvalue_reference<T>::value)
r += "&";
else if (std::is_rvalue_reference<T>::value)
r += "&&";
return r;
}

这样使用该函数:type_name<decltype(y)>(),其中y就是使用auto声明的变量。

1:类型饰词是引用或指针,但不是万能引用

int x = ;
int &rx = x;
const int &crx = x;
auto &ax = x; //type is int&
auto &arx = rx; //type is int&
auto &acrx = crx; //type is const int& const int y = ;
auto &ay = y; //type is const int& int &&rr = ;
auto &arr = rr; //type is int& const int &&crr = ;
auto &acrr = crr; //type is const int& int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
auto &ap = p; //type is int*&
auto &acp = cp; //type is const int*&
auto &accp = ccp; //type is const char * const &

下面是const auto的情况

int x = ;
int &rx = x;
const int &crx = x;
const auto &ax = x; //type is const int&
const auto &arx = rx; //type is const int&
const auto &acrx = crx; //type is const int& const int y = ;
const auto &ay = y; //type is const int& int &&rr = ;
const auto &arr = rr; //type is const int& const int &&crr = ;
const auto &acrr = crr; //type is const int& int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
const auto &ap = p; //type is int* const &
const auto &acp = cp; //type is const int* const &
const auto &accp = ccp; //type is const char* const &

下面是指针的情况:

int x = ;
int *p = &x;
const int *cip = &x;
int const *cip2 = &x;
int * const icp = &x;
const char * const ccp = "abcd";
auto *ap = p; //type is int*
auto *acip = cip; //type is const int*
auto *acip2 = cip2; //type is const int*
auto *aicp = icp; //type is int*
auto *accp = ccp; //type is const char* const auto *cap = p; //type is const int*
const auto *cacip = cip; //type is const int*
const auto *cacip2 = cip2; //type is const int*
const auto *caicp = icp; //type is const int*
const auto *caccp = ccp; //type is const char*

以上的结果和模板推导规则是一样的。

2:类型饰词是万能引用

int x = ;
int &rx = x;
const int &crx = x;
auto &&ax = x; //type is int&
auto &&arx = rx; //type is int&
auto &&acrx = crx; //type is const int& const int y = ;
auto &&ay = y; //type is const int& int &&rr = ;
auto &&arr = rr; //type is int& const int &&crr = ;
auto &&acrr = crr; //type is const int& int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
auto &&ap = p; //type is int*&
auto &&acp = cp; //type is const int*&
auto &&accp = ccp; //type is const char * const & auto &&a1 = std::move(x); //type is int&&
auto &&a2 = std::move(y); //type is const int&&
auto &&a3 = std::move(rr); //type is int&&
auto &&a4 = std::move(p); //type is int*&&
auto &&a5 = ; //type is int&&

以上的结果和模板推导规则是一样的。

3:类型饰词既不是指针也不是引用

int x = ;
int &rx = x;
const int &crx = x;
auto ax = x; //type is int
auto arx = rx; //type is int
auto acrx = crx; //type is int const int y = ;
auto ay = y; //type is int int &&rr = ;
auto arr = rr; //type is int const int &&crr = ;
auto acrr = crr; //type is int int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
auto ap = p; //type is int*
auto acp = cp; //type is const int*
auto accp = ccp; //type is const char * auto a1 = ; //type is int

下面是const auto的情况:

int x = ;
int &rx = x;
const int &crx = x;
const auto ax = x; //type is const int
const auto arx = rx; //type is const int
const auto acrx = crx; //type is const int const int y = ;
const auto ay = y; //type is const int int &&rr = ;
const auto arr = rr; //type is const int const int &&crr = ;
const auto acrr = crr; //type is const int int *p = &x;
const int *cp = &x;
const char * const ccp = "abcd";
const auto ap = p; //type is int* const
const auto acp = cp; //type is const int* const
const auto accp = ccp; //type is const char * const const auto a1 = ; //type is const int

以上的结果和模板推导规则是一样的。

4:数组实参

const char name[] = "J. P. Briggs";
auto aname = name; //type is const char*
auto &arname = name; //type is const char (&) [13]

以上的结果和模板推导规则是一样的。

5:函数实参

类似于数组,函数类型也会退化为函数指针,并且针对数组类型推导的一切结论,也适用于函数。

void someFunc(int, double); 

auto asomefunc = someFunc; //type is void (*)(int, double)
auto &arsomefunc = someFunc; //type is void (&)(int, double)

以上的结果和模板推导规则是一样的。

6:例外情况

在C++11之前,有两种初始化方法,直接初始化和复制初始化:

int x1 = ;
int x2();

在C++11中,引入了统一初始化(uniform initialization),从而增加了两种初始化方式,直接链表初始化和复制链表初始化:

int x3 = {}; // copy list initialization
int x4{}; // direct list initialization

如果将上面的初始化换成auto:

auto x1 = ;
auto x2();
auto x3 = {};
auto x4{};

此时,x1和x2的类型仍然是int,但是x3和x4的类型却是std::initializer_list<int>,且含有单个值为27的元素。这就是auto类型推导的特殊规则,当auto声明的变量的初始化表达式使用大括号括起时,推导的类型就属于std::initializer_list。

在c++17之前,auto遇到{}就直接推导为std::initializer_list,但是从C++17开始,规则发生了变化:In direct-list-initialization (but not in copy-list-initalization), when deducing the meaning of the auto from a braced-init-list, the braced-init-list must contain only one element, and the type of auto will be the type of that element。

也就是说,复制链表初始化的推导规则不变,但是对于直接链表初始化,{}中只允许包含一个元素,而推导结果为该元素的类型:

auto x1 = {}; // x1 is std::initializer_list<int>
auto x2{, }; // error: not a single element
auto x3{}; // x3 is int (before N3922 x2 and x3 were both std::initializer_list<int>)

https://en.cppreference.com/w/cpp/language/template_argument_deduction#Other_contexts

https://mariusbancila.ro/blog/2017/04/13/cpp17-new-rules-for-auto-deduction-from-braced-init-list/

如果向相应的模板传入这样的一个初始化表达式,则类型推导失败,代码编译错误:

auto x = { , ,  }; // x's type is std::initializer_list<int>

template<typename T>
void f(T param);
f({ , , }); // error! can't deduce type for T

但是,如果模板中param为std::initializer_list<T>,则可以进行这样的推导:

template<typename T>
void f(std::initializer_list<T> initList); f({ , , }); // T deduced as int, and initList's type is std::initializer_list<int>

在C++14中,允许使用auto来说明函数返回值需要推导;允许lambda的形参声明中使用auto。但是,这两种auto用法使用的是模板推导规则,因此,下面的代码都会报编译错误:

auto createInitList() {
return { , , }; // error: can't deduce type for { 1, 2, 3 }
} std::vector<int> v;
auto resetV = [&v](const auto& newValue) { v = newValue; };
resetV({ , , }); // error! can't deduce type for { 1, 2, 3 }

最后需要注意的是,使用统一初始化时初始化auto变量时,大括号内的类型应该一致,否则会发生编译错误:

auto x5 = { , , 3.0 }; // error! can't deduce T for std::initializer_list<T>

03decltype类型指示符

如果希望从表达式的类型推断出要定义的变量类型,但不想用表达式的值初始化变量,C++11引入了decltype,它的作用就是选择并返回操作数的数据类型,编译器分析并得到它的类型,却不实际计算表达式的值:decltype(f()) sum = x; 这条语句中,sum的类型就是函数f的返回类型。但是编译器并不实际调用函数f。

对于给定的名字或表达式,decltype可以得到名字或表达式的类型。decltype的类型推导规则与auto不同:

如果decltype使用的表达式是一个单纯的变量,则decltype直接返回该变量的类型(包括顶层const和引用在内);

如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型;

如果表达式的内容是解引用操作,则decltype将得到引用类型;

const int i = ; // decltype(i) is const int

bool f(const Widget& w); // decltype(w) is const Widget&
// decltype(f) is bool(const Widget&)
struct Point {
int x, y; // decltype(Point::x) is int; decltype(Point::y) is int
}; Widget w; // decltype(w) is Widget if (f(w)) … // decltype(f(w)) is bool template<typename T>
class vector {
public:

T& operator[](std::size_t index);

};
vector<int> v; // decltype(v) is vector<int>
if (v[] == ) … // decltype(v[0]) is int& int *p = &x; // decltype(p) is int*; decltype(*p) is int&

对于decltype所用的表达式来说,如果decltype使用的是一个不加括号的变量,则结果是变量类型;如果加上了一个或多层括号,则编译器将其当成一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以decltype就会得到引用类型。

尽管decltype可能会得出以外的类型推导结果,但是这是非正常情形,在正常情形下,decltype得到的类型就是你期望的类型。

在C++11中,decltype的主要用途大概就在于声明那些返回值类型依赖于形参类型的函数模板。比如现在要写一个函数,形参是一个支持[]操作的容器,以及一个下标索引,函数返回值需要与[]操作的返回值类型相同。

下面是使用decltype计算返回值类型,它还有改进空间:

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
-> decltype(c[i]) {
return c[i];
}

这里的auto和类型推导没有任何关系,它只是说明这里使用了C++11中的返回值类型尾序语法(trailing return type synax),即该函数的返回值类型将在形参表之后,确切的说是在”->”之后。尾序返回值的好处在于指定返回值类型时可以使用函数形参。

在C++14中,允许使用auto推导返回值类型,但是如果这样写:

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i){
return c[i]; // return type deduced from c[i]
}

这种写法会有问题,根据模板推导规则3,初始化表达式的引用性会被忽略:

std::deque<int> d;
// compile error: invalid initialization of non-const
// reference of type ‘int&’ from an rvalue of type
authAndAccess(d, ) = ;

d[5]返回int&,但是返回值类型却是int,作为返回值,该int是个右值,无法为其赋值。

为了使authAndAccess如期运作,在C++14中引入了decltype(auto),指定类型推导时需采用decltype推导规则:

template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container& c, Index i)
{
return c[i];
}

现在,authAndAccess返回值类型和c[i]一致了。

decltype(auto)并不限于在函数返回值使用,在变量声明时,如果也想在初始化表达式处使用decltype类型推导规则,也可以这样:

Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto type deduction: myWidget1's type is Widget
decltype(auto) myWidget2 = cw; // decltype type deduction: myWidget2's type is const Widget&

之前的写法还有一个问题,就是authAndAccess无法接收右值容器,右值不能绑定到左值引用上,如果需要既支持左值引用,又要支持右值容器,一种方法是使用重载(一个函数声明一个左值引用形参,另一个函数声明一个右值引用形参),但是这样的做法需要维护两个函数;还有一种更好的方法就是使用万能引用:

template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container&& c, Index i){
return std::forward<Container>(c)[i];
}

万能引用需要使用std::forward,原因会在条款25中解释。以上是C++14的写法,如果是C++11,则:

template<typename Container, typename Index>
auto authAndAccess(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i]){
return std::forward<Container>(c)[i];
}

之前提到过,对于int类型的x而言,decltype(x)的结果是int,但是如果是decltype((x)),表达式(x)是一个左值,所以decltype((x))的结果就是int&。这种形式如果和decltype(auto)结合,就有可能引起问题,下面的代码中,f2返回了一个局部引用:

decltype(auto) f1(){
int x = ;
return x; // decltype(x) is int, so f1 returns int
}
decltype(auto) f2(){
int x = ;
return (x); // decltype((x)) is int&, so f2 returns int&
}

04:查看类型推导结果的方法

之前的条款中,已经介绍了几种查看类型推导的方法,还有其他几种方法:

1:IDE编辑器

使用IDE时,鼠标悬停置某个变量上时,就会显示出该变量的类型。这种方法面对简单类型时很有用,但是一旦面对较为复杂的类型,IDE显示的信息就不太有用了。

2:编译器诊断信息

想要让编译器显示其推导出的类型,一种有效的方法是使用该类型导致某种编译错误,利用错误信息,得到推导的结果。

先声明一个模板:

template<typename T>
class TD;

只要试图具现该模板,就会产生一条错误信息,原因是找不到具现模板的定义。比如想要查看x和y的类型,尝试用x和y的类型具现化该模板即可:

TD<decltype(x)> xType;
TD<decltype(y)> yType;

针对上述代码,编译器可能报错如下:

error: aggregate 'TD<int> xType' has incomplete type and cannot be defined
error: aggregate 'TD<const int *> yType' has incomplete type and cannot be defined

根据这种错误信息,就能得到x和y的类型。

3:运行时输出

想要运行时输出类型信息,第一反应可能是使用std::type_info::name:

std::cout << typeid(x).name() << '\n';
std::cout << typeid(y).name() << '\n';

对于std::type_info::name的调用不保证返回任何有意义的内容,不同的编译器返回的内容也不尽相同,不如GUN和Clang的编译器的结果,x的类型是i,y类型是PKi,i表示int,PKi表示pointer to const *;而visual studio返回的结果是int和int const*。

面对简单类型这种方法还能勉强应付,但是针对复杂类型,就有可能出现问题:

template<typename T>
void f(const T& param) {
using std::cout;
cout << "T = " << typeid(T).name() << '\n'; // show T
cout << "param = " << typeid(param).name() << '\n'; // show
} std::vector<Widget> createVec();
const auto vw = createVec();
if (!vw.empty()) {
f(&vw[]);
}

GUN和Clang编译产生的结果可能是:

T = PK6Widget
param = PK6Widget

结果的意义是:pointer to const Widget。如果visual studio,则结果是:

T = class Widget const *
param = class Widget const *

结果是一样的,但是根据之前介绍的模板推导规则,这些结果显然是不正确的。但是这种结果是符合标准的,标准规定,std::type_info::name处理类型的方式就像是向函数模板按值传递形参一样。

使用IDE的话,上面的例子可能的返回T的类型结果如下:

const
std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *

而param的类型是:

const std::_Simple_types<...>::value_type *const &

这种情况下,使用Boost的TypeIndex库可以得到正确的结果:

#include <boost/type_index.hpp>
template<typename T>
void f(const T& param){
using std::cout;
using boost::typeindex::type_id_with_cvr; cout << "T = " // show T
<< type_id_with_cvr<T>().pretty_name()
<< '\n'; cout << "param = " // show param's type
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}

利用这个f的实现,测试上面的那个例子,得到的结果是正确的。GNU和Clang的结果如下:

T = Widget const*
param = Widget const* const&

visual studio的结果大同小异:

T = class Widget const *
param = class Widget const * const &

所以,利用IDE编译器、编译器报错,以及Boost.TypeIndex,都可以查看类型推导的结果。但是,某些工具产生的结果可能是错误的,所以理解C++类型推导规则是必要的。

上一篇:从 mian 函数开始一步一步分析 nginx 执行流程(四)


下一篇:25.C++- 泛型编程之函数模板(详解)