文章目录

  • ​​0 背景​​
  • ​​1 手工推导类型推导​​
  • ​​1.1 根据形参类型​​
  • ​​1.1.1 ParamType是指针或引用​​
  • ​​1.1.2 ParamType是万能引用​​
  • ​​1.1.3 ParamType既非指针也非引用(按值传递)​​
  • ​​1.2 根据实参(仅当实参为数组/函数)​​
  • ​​1.2.1 模版推导数组长度​​
  • ​​2 auto​​
  • ​​2.1 使用特例​​
  • ​​2.2 使用方法​​
  • ​​2.3类型推导不符合要求的,使用显示类型转换​​
  • ​​3 decltype用法​​
  • ​​4 查询类型推导的结果​​
  • ​​5 延伸(获得数组长度)​​

0 背景

1 手工推导类型推导

模版:

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

调用函数:

f(expr);//调用f

模版类型推导的含义就是编译器会通过expr推导两个类型:T和ParamType,两个类型通常不一样。因为ParamType的类型常会包括一些修饰词,如const。

具体例子如下:

template<typename T>
void f(const T& param);
int x = 0;
f(x);

得到的类型为:

T     =  int
param = const int&

根据ParamType的不同,类型推导可以分为三种情况:

  • 1,ParamType是指针或引用;
  • 2,ParamType是万能引用;
  • 3,ParamType既非指针既非引用。

1.1 根据形参类型

1.1.1 ParamType是指针或引用

类型推导方法:


1,若expr具有引用类型,则先将引用部分忽略;
2,然后,对expr的类型和ParamType的类型执行模式匹配,来决定T的类型。


  • 当类型为引用类型:

例如下面的例子中,

下面三个变量的T和param的类型分别是:

变量名

T类型

param类型

x【类型:int】

int

int&

cx【类型:const int】

const int

const int&

rx【类型:const int&】

const int

const int&

template <typename T>
void f(T& param);
int x = 0;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);

rx的引用在推导时被忽略,印证了法则1。

  • 当类型为指针类型,

下面两个变量的类型如下所示:

变量名

T类型

param类型

x【int】

int

int*

px【const int*】

const int

const int*

template <typename T>
void f(T* param);
int x = 0;
const int* px = &x;
f(x);
f(px);

1.1.2 ParamType是万能引用

​​万能引用​​

类型推导方法:


1,如果expr是左值,T和ParamType则都会被推导为左值引用;
2,如果expr是右值,则和1.1内容相同。


例如,下面变量的推导类型为:

变量名

T类型

param类型

x【int】

int &

int&

cx【const int】

const int&

const int&

rx【const int&】

const int&

const int&

27【&&】

int

int&&

template <typename T>
void f(T&& param);
int x = 0;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);
f(27);

1.1.3 ParamType既非指针也非引用(按值传递)

因为是按值传递,所以param就是传入的一个副本,即一个全新的对象,这从而形成全新的推导方法。

类型推导方法:

  • 1,若expr具有引用类型,则忽略其引用部分;
  • 2,然后,若expr是一个const对象,也忽略之,若expr是volatile对象,同忽略之。

例如,下面变量的推导类型为:

变量名

T类型

param类型

x【int】

int

int

cx【const int】

int

int

rx【const int&】

int

int

template <typename T>
void f(T param);
int x = 0;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);

⚠️:const(和volatile)仅在按值形参处被忽略 。如果是const引用或指针,expr的常量性则在推导的过程中被保留。

例如下面的例子中,形参为​​const char* const​​类型,则星号左侧的const类型则会被保留,星号左侧的const表明ptr指向的对象不能被更改,星号右侧的const表明ptr不可指向其他内存。

也就是在类型推导中,ptr所指对象的常量性会被保留。

变量名

T类型

param类型

ptr

const char*

const char*

template <typename T>
void f(T param);
const char* const ptr = "hahahah";
f(ptr);

1.2 根据实参(仅当实参为数组/函数)

当行参为按值传递的模版时,数组将会退化为指向首元素的指针。

例如,在下面的例子中,变量被推导为:

变量名

T类型

param类型

name

const char*

const char*

template <typename T>
void f(T param)
const  char name[] = "jiangxueHan";
const char* ptrToName = name;//数组退化指针的形式
f(name);

如果想要传递真正的数组,就需要把形参模版改为引用类型,然后按值传递实参。这样就会得到真正意义上的数组类型,因为还包含着数组的长度。

例如,在下面的例子中【按引用传递】,变量被推导为下面的结果:【字符常量为11位,因为还包含一个结尾字符​​\0​​,所以长度为12】

变量名

T类型

param类型

name

const char[12]

const char(&)[12]

template <typename T>
void f(T& param)
const  char name[] = "jiangxueHan";
f(name);

由于函数类型也可以退化为函数指针,所以对数组的类型推导也适用于函数。

下面的例子中【按值传递】,函数被推导为下面的结果:

变量名

T类型

param类型

func

void (*)(int, double)

void (*)(int, double)

template <typename T>
void f(T param);
void func(int, double){}
f(func);

下面的例子中【按引用传递】,函数被推导为下面的结果:

变量名

T类型

param类型

func

void (int, double)

void (&)(int, double)

template <typename T>
void f(T& param);
void func(int, double){}
f(func);

1.2.1 模版推导数组长度

利用引用案例的特性,我们可以创造一个模版来推到数组长度。

template <typename  T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}

代码中,constexpr用于让代码在编译期间就可以使用值,noexcept用于让编译器知道不会抛出异常,从而得到更多的优化。

2 auto

auto其实就等同于类型推导,模版类型推导和auto存在一一映射关系。

映射关系如表所示:

auto类型

模版推导类型

auto x = 27;

template< typename T> void fun_for_x(T parma);

const auto cx = x;

template< typename T> void fun_for_x(const T parma);

const auto& cx = x;

template< typename T> void fun_for_x(const T& parma);

2.1 使用特例

规则:当auto声明变量的初始化表达式是使用大括号括起来时,推导所得的类型就是std::initializer_list。如果类型推导失败(大括号中的值类型不一样,​​auto x = {1, 2.3, 4};​​),代码就通不过编译。

例子:

初始化一个int的方法为:

C++98:

int x = 27;
int x2(27);

C++11:(为了支持统一化)

int x3 = {27};
int x4{27};

如果使用auto来进行变量声明:

auto x1 = 27;//类型为int
auto x2(27);//类型为int

auto x3 = {27};//类型为std::initializer_list<int>
auto x4{27};//类型为std::initializer_list<int>

所以auto和模版类型推到的唯一区别就是,auto会假定大括号括起来的初始化表达式代表一个​​std::initializer_list​​,但模版推导不会。

例如:

auto x = {11, 23, 9};//类型为std::initializer_list<int>

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

}

f({11, 23, 9});//错误,无法类型推导【T类型推导为int,而参数的类型为std::initializer_list<int>】

//正确模版
//template<typename T>
//void f(std::initializer_list<T> param){
//
//}

列子二:

std::vector<int> v;
auto resetV = [&v](const auto & newValue){
v = newValue;//error: no matching function for call to object of type
};
resetV({1,2,3,});

⚠️:在C++14中,C++14允许函数返回类型使用auto推导,C++14中的lambda式的形参也会使用auto,但是这些auto用法是在使用模版类型推导而非auto类型推导,因此带有auto返回类值的函数要返回一个大括号括起来的初始化表达式是编译不通过的。

2.2 使用方法

auto声明的变量都必须初始化,例如:

auto x2;//编译错误
auto x3 = 0;正确

使用auto代替迭代器解引用【提领】,例如:

template<typename It>
void dwim(It b, It e){
while(b != e){
typename std::iterator_traits<It>::value_type currValue = *b;
//...
}
}

template<typename It>
void dwim2(It b, It e){
while(b != e){
auto currValue = *b;
//...
}
}

声明、存储闭包。使用auto声明、存储的闭包和该闭包同类型,内存量也和闭包相同;使用std::function声明、存储的闭包,都是std::function的一个实例,都占有固定的尺寸,如果该尺寸不够存储该闭包,就会分配堆上面的内存。例如:

std::function<bool(const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)> dereUPless = [](const std::unique_ptr<Widget>& p1, const std::unique<Widget>& p2){
return *p1 < *p2;
}
auto derefLess = [](const auto& p1, const auto& p2 ){
return *p1 < *p2;
}

避免显示指定类型导致意外结果。例如:

std::vector<int> v;
unsigned sz = v.size();

这个例子,在32位Windows下,unsigned是32位和​​std::vector<int>::size_type​​​相同,但是在64位Windows下,​​std::vector<int>::size_type​​​是64位,而​​unsigned​​​是32位,就会出现表现异常。合理方法应该是使用​​auto sz = v.size();​

还有一个例子如下。在下面的例子中,由于std::unordered_map的键值部分是const,所以std::pair的类型应该是std::pair<const std::string, int>。但是下面写的却是string类型,编译器实际在运行时,做类型转换,会把m中每个对象复制一次,把p绑定到临时对象,循环迭代结束时,该临时对象会被析构一次。但是如果使用auto,就不会这样的情况。

std::unordered_map<std::string, int> m;
for(const std::pair<std::string, int>& p: m){
//...
}

//解决
for(const auto& p:m){}

2.3类型推导不符合要求的,使用显示类型转换

一般对于含有类型T的对象容器,其operator[]会返回​​T&​​​,但是​​std::vector< bool>​​​ 对于的​​operator[]​​​返回的不是​​bool&​​​【因为​​std::vector< bool>​​​做过特化,每个bool用一比特标识,但是C++禁止比特的引用】,而是​​std::vector< bool>::reference​​​【嵌套在​​std::vector< bool>​​​中的类】。为了保证使用​​std::vector< bool>​​​返回的像​​bool&​​​,把​​std::vector< bool>::reference​​做了一个向bool的隐式类型转换。

class Widget{};
std::vector<bool> features(const Widget& w){}
Widget w;
bool highPriority = features(w)[5];
processWidget(w, highPriority);//合法

下面为定义的原因如下,freature返回一个​​std::vector< bool>​​​类型,然后​​operator[]​​​返回一个​​std::vector< bool>::reference​​​类型,而auto会把highPriority推导为​​std::vector< bool>::reference​​​类型。features会返回一个​​std::vector< bool>::reference​​类型的临时对象,在该行表达式结束的时候,指向的对象会被析构。在processWidget中highPriority是一个空悬指针,最终导致未定义。

class Widget{};
std::vector<bool> features(const Widget& w){
return std::vector<bool> {true, false, true, true, false, true};
}

Widget w;
auto highPriority = features(w)[5];
processWidget(w, highPriority);//未定义【待验证】

解决方法:

class Widget{};
std::vector<bool> features(const Widget& w){
return std::vector<bool> {true, false, true, true, false, true};
}
Widget w;
auto highPriority = static_cast<bool>(features(w)[5]);//在临时对象被析构之前,就是解引用得到值
processWidget(w, highPriority);//正常

隐式代理类类,无法和auto和平共处。防止写出​​auto someVar = '隐式代理类型表达式'​​ 。例如,

Matrix sum = m1 + m2 +m3 + m4;

两个Matrix 对象的operator+返回的是一个代理类对象【比如表达式模版】,比如​​Sum< Matrixm, matrix>​​z ,而不是Matrix【表达式模版技术,返回的是结果的代理而不是结果】。

解决方法:

auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

使用显示类型转化,来创建一个有异于初始化表达式的变量。

double calEpsilon();
auto ep = static_cast<float>(calEpsilon());//显示表明自己降低了精度

如何发现隐式代理类,通过查阅相关文档或查看头文件中的代码,函数签名往往会反映它们的存在。例如:

namespace std{
tempalte<class Allocator>
class vector<bool, Allocator>{
class reference{
//...
}
reference operator[](size_type n);
}
}

3 decltype用法

decltype的主要用途是声明函数的返回值类型依赖于形参类型的函数模版。

  • 1,示例一【声明返回类型】
    C++11中的用法,使用的是返回值别尾序法,这样的好处在于指定返回类型时可以使用函数形参(不然下面的例子,c和i就无法使用):
template<typename  Container, typename Index>
auto authAnAccess(Container&& c, Index i)->decltype(c[i]){
authenticateUser();
return std::forward<Container>(c)[i];
}

使用C++14版本:

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

对于auto类型推导出来类型不符合意愿的,可以使用decltype。例如

Widget w;
const Widget& cw = w;
auto myWidget = cw;//推导出来的类型为Widget
decltype(auto) myWidget2 = cw;//推导出来的类型为const Widget&
  • 2,示例二【使用在元编程(模版编程)中】
template <typename  T>
T test(T obj){//默认传进来的为容器
//获得迭代器
typedef typename decltype(obj)::iterator iType;
//等价于
typedef typename T::iterator iType2;


}
  • 3,示例三【用于求lambda表达式的类型:】
private:
int m_data;
public:
A(int _data):m_data(_data){}
int getData() const{ return m_data;}
};
int main(int argc, char** argv){
//对set进行自定义排序
auto cmp = [](const A& a, const A& b){
return a.getData() < b.getData();
};
std::set<A, decltype(cmp)> s(cmp);
A a(2);
A b(20);
A c(15);
s.insert(a);
s.insert(b);
s.insert(c);
for(auto item:s){
cout<<item.getData()<<" ";
}

return 0;
}

⚠️注意:decltype作用于仅比名字更复杂的左值表达式时,它能保证类型总是左值引用。也就是一个左值表达式不仅是一个类型为T的名字,它就会得出一个T&的类型。

例如:x是一个左值,类型为int,表达式(x)也是左值,所以decltype((x))的结果是int&。【下面的列子,仅仅把名字放入一对小括号,就改变了decltype的推导结果】

decltype(auto) f1(){
int x;
//...
return x;//decltype(x)是int
}

decltype(auto) f2(){
int x;
//...
return (x);//decltype(x)是int&
}

4 查询类型推导的结果

方法一:

template <typename T>
void f2(const T& param)
{
cout<<"T = "<< typeid(T).name()<<"\n";
cout<<"param = "<< typeid(param).name()<<"\n";
}

方法二【自己下载和安装boost库】:

#include <boost/type_index.hpp>

template <typename T>
void f(T& param){
using boost::typeindex::type_id_with_cvr;
// 显示T类型
cout<<"T = "<<type_id_with_cvr<T>().pretty_name()<<"\n";
//显示param类型
cout<<"param = "<<type_id_with_cvr<decltype(param)>().pretty_name()<<"\n";
}

5 延伸(获得数组长度)

template <typename  T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}

使用实例:

std::cout<<arraySize(name);