1 模板推导规则

1.1 函数的形参是一个指针或引用但不是通用引用

  • 函数模板推导时,推导出的T会省略函数模板中形参类型有的部分(const、*、&)。

  • 实参类型有,函数模板中形参类型没有的部分,推导出的T则会保留这一部分。

  • 最后推导出的param的类型,将会是实参类型和函数模板中形参类型的并集。

demo:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <boost/type_index.hpp> //主要通过boost库来打印完整的数据类型

template<typename T>
void test1(T& param)
{
std::cout<< "T:" << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;

std::cout<< "形参:" << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() << std::endl;
}

template<typename T>
void test2(const T& param)
{
std::cout<< "T:" << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;

std::cout<< "形参:" << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() << std::endl;
}

template<typename T>
void test3(const T* param)
{
std::cout<< "T:" << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;

std::cout<< "形参:" << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() << std::endl;
}

int main()
{
int a = 10;
const int b = a;
const int& c = a;
const int* p = &a;

std::cout << "test1:" << std::endl;
test1(a);
test1(b);
test1(c);

std::cout << "test2:" << std::endl;
test2(a);
test2(b);
test2(c);

std::cout << "test3:" << std::endl;
test3(&a);
test3(p);

return 0;
}

结果:

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
test1:
T:int
形参:int&
T:int const
形参:int const&
T:int const
形参:int const&
test2:
T:int
形参:int const&
T:int
形参:int const&
T:int
形参:int const&
test3:
T:int
形参:int const*
T:int
形参:int const*

1.2 函数的形参是通用引用

  • 如果传入的实参是左值,无论T还是param都会被推导为左值引用。

  • 如果传入的实参是右值,则会和1.1的规则一样。

demo:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <boost/type_index.hpp>

//函数模板形参为通用引用
template<typename T>
void test4(T&& param)
{
std::cout<< "T:" << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;

std::cout<< "param:" << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() << std::endl;
}

int main()
{
int a = 10;
const int b = a;
const int& c = a;

std::cout << "T&&:" << std::endl;
test4(a);
test4(b);
test4(c);
test4(10);

return 0;
}

结果:

plaintext
1
2
3
4
5
6
7
8
9
T&&:
T:int&
param:int&
T:int const&
param:int const&
T:int const&
param:int const&
T:int
param:int&&

1.3 函数的形参是值传递

  • 无论传递什么实参,param都会是作为它的拷贝,T和param基本一致,并且一般会忽略引用性 &常量性 const

  • 但如果实参是指针,则将地址完全拷贝进param,因此param依旧是指针,但会省略其右const(不能修改指针指向)的常量性而不会省略其左const(不能修改指针指向的值)的常量性

demo:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <boost/type_index.hpp>

template<typename T>
void test5(T param)
{
std::cout<< "T:" << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;

std::cout<< "param:" << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() << std::endl;
}

int main()
{
int a = 10;
const int b = a;
const int& c = a;

const int* p1 = &a;
int* const p2 = &a;
const char* const p3 = "既是常量指针,也是指针常量";

std::cout << "值传递" << std::endl;

std::cout << "T param:" << std::endl;
test5(a);
test5(b);
test5(c);
test5(p1);
test5(p2);
test5(p3);

return 0;
}

结果:

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
值传递
T param:
T:int
param:int
T:int
param:int
T:int
param:int
T:int const*
param:int const*
T:int*
param:int*
T:char const*
param:char const*

1.4 其他:数组作为实参

由于即使函数的形参声明为数组:void myFunc(int param[]),也会被视为指针声明:void myFunc(int* param),因此以数组为实参通过值传递传入函数模板中,则依旧会被推导为指针。

cpp
1
2
3
4
int nums[] = {1, 2, 3, 4};
template<typename T>
void f(T param);
//T和param推导为int*

而如果形参为引用传递,传入数组实参则会被推导为数组。

cpp
1
2
3
4
int nums[] = {1, 2, 3, 4};
template<typename T>
void f(T& param);
//T推导为int[4],param被推导为int(&)[4]

1.5 其他:函数作为实参

一般函数作为参数传入函数时,是会退化为指向函数的指针,也就是通过函数指针方式传入。

于是针对以下函数传入以下的函数模板中,如果是值传递,param就会被推导为函数指针,如果是引用传递,则会是函数的引用。

cpp
1
2
3
4
5
6
7
8
9
10
void someFunc(int, double);

template<typename T>
void f1(T param)

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

f1(someFunc); //param为void(*) (int, double)
f2(someFunc); //param为void(&) (int, double)

注意:

函数和数组因为会被推导为指针,所以有左const也不会被省略。
数组被推导为指针时会丢失大小信息,因此最好还是用STL容器。

2 auto

2.1 与模板推导规则相同点

  • 情况一:变量不是引用、指针、通用引用,不是函数,不是数组。
cpp
1
2
3
auto x = 27; //x类型为int
const auto cx = x; //cx类型为const int
const auto& rx = cx; //rx类型为const int&
  • 情况二:变量是引用。类似模板推导中的值传递,传带引用性的实参,引用性被忽略。
cpp
1
2
3
int x = 27;
int& rx = x;
auto ax = rx; //ax的类型为int,把rx的引用性忽略了。
  • 情况三:变量是通用引用
cpp
1
2
3
4
5
6
7
auto x = 27;
const auto cx = x;
const auto& rx = cx;

auto&& uref1 = x; //x本身是一个int的左值,所以uref1类型为int&
auto&& uref2 = cx; //cx是一个const int的左值,所以unref2类型为const int&
auto&& uref3 = 27; //27是右值,所以uref3的类型为int&&
  • 情况四:数组
cpp
1
2
3
const char name[] = "hanmeilin";
auto arr1 = name; //arr1的类型为const char*
auto& arr2 = name; //arr2的类型为const char(&) [9]
  • 情况五:函数
cpp
1
2
3
void someFunc(int, double); 
auto func1 = someFunc; //func1的类型为void(*) (int, double)
auto& func2 = someFunc; //func2的类型为void(&) (int, double)

2.2 与模板推导规则不同点:对统一初始化的推导

  • auto针对统一初始化,会推导为std::initializer_list
cpp
1
2
3
4
5
6
7
auto x1 = {27};
auto x2{27};
//此时值为{27},推导出的类型为std::initializer_list<int>


auto x3 = {1, 2, 3.0};
//此时会编译报错,因为有不同类型的
  • 模板推导对于统一初始化,则推导不出来,会报错。
cpp
1
2
3
4
template<typename T>
void f(T param);

f({11, 2, 9}); //报错
  • 在模板中将T指定为std::initializer_list<T>,则能使模板类型推导正常工作。
cpp
1
2
3
4
template<typename T>
void f(std::initializer_list<T> initList);

f({11, 22, 9}); //能正常工作,这里面的T会被推到为int

2.3 C++14中auto作为函数返回值或lambda函数形参

这里auto的实际工作机制是模板类型推导,所以此时用auto推导花括号的统一初始化,反而会报错。

auto做函数返回值:

cpp
1
2
3
4
auto createInitList()
{
return {1, 2, 3}; //报错
}

auto做lambda函数形参:

cpp
1
2
3
4
5
std::vector<int> v
...
auto resetV = [&v] (const auto& newValue) {v = newValue;};
...
resetV({1, 2, 3}); //报错

2.4 使用auto的好处

  • 避免未初始化的无效变量
cpp
1
2
int x; //x没有初始化,此时它的值是不能确定的
auto ax = 0; //auto要求声明变量时必须要初始化
  • 省略冗长的声明类型,并且有些表达式或函数返回的类型只有编译器知道,也可以用auto声明。
cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
//这个函数设定传入的参数是迭代器
template<typename It>
void dwim(It b, It e)
{
//这行代码中间的这一部分是一个类型,且它依赖于模板的参数It,所以是依赖类型,前面需要加typename
//std::iterator_traits<It>获取类型为It迭代器的信息
//value_type获取迭代器指向元素的类型
//*b解引用,获取迭代器b指向的具体的值
typename std::iterator_traits<It>::value_type currValue1 = *b;

//而这个长的类型声明可以直接由一个auto替代
auto currValue2 = *b;
}
  • 对于保存闭包,相对于std::function语法更加简洁,并且内存消耗可能会更少。因为auto会直接使用与闭包相同大小的存储空间,而实例化std::function时会有一个固定的大小,如果这个大小不足以存储一个闭包,则std::function的构造函数会在堆上分配内存来存储,因此会消耗更多的内存。

闭包由要执行的代码块作用域(计算环境)两个部分组成,在C++中,通常由lambda表达式生成。

cpp
1
2
3
4
auto x1 = [](int x, int y){return x + y;}

//std::function可以指向任何可调用的对象,这里主要作为闭包对象来存储闭包
std::function<int(int, int)> x2 = [](int x, int y){return x + y;}
  • 避免无意识的类型不匹配错误

demo1:

cpp
1
2
3
4
5
6
7
std::vector<int> v;

//v1.size()的标准返回类型是std::vector<int>::size_type
//它在64位系统上大小是64位,在32位系统上是32位
//而unsigned int的大小始终是32位,在64位系统上就会出现错误
unsigned sz = v1.size();
auto sz = v2.size();

demo2:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//对于std::unordered_map<std::string, int> m
//它的键应该是const的
//所以它的每个子元素的类型应该是std::pair<const std::string, int>
//而不是std::pair<std::string, int>
//但这样也会编译成功,编译器会通过拷贝m中的std::pair<const std::string, int>
//生成一个std::pair<std::string, int>的临时对象
//而p则是指向临时变量的引用,和本意相违背
std::unordered_map<std::string, int> m;
for (const std::pair<std::string, int>& p : m)
{
}

//使用auto可以直接避免这个问题
//甚至这时候对p取地址,能真的取到m中对应元素的地址
for (const auto& p : m)
{
}

2.5 使用static_cast强制auto推导出想要的结果

使用此操作的问题背景:

cpp
1
2
3
4
5
6
7
std::vector<bool> features(const Widget& w);
void processWidget(const Widget& w, bool highPriority);
Widget w;
//features(w)[5]返回的不是bool&类型,而是C++中对于bool&的代理类,且它是不可见的
auto highPriority = features(w)[5];
//此时highPriority传入processWidget中就会出现未定义的行为
processWidget(w, highPriority);

解决:

cpp
1
2
3
//不限制右边的表达式产生不可见代理类(因为代理类是有优化好处的),但也想继续使用auto(因为auto有前面所述的诸多好处)
//使用static_cast得出期望的推导结果
auto highPriority = static_cast<bool>features(w)[5];

3 decltype

作用:decltype(变量)可以获取到变量的类型。

3.1 decltype(auto)

C++14后添加

作用: decltype(auto)会根据decltype的规则推导出变量完整的类型。这里的auto表示这个类型将会被推导。

demo1:

cpp
1
2
3
4
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; //auto推导类型为Widget
decltype(auto) myWidget2 = cw; //decltype(auto)推导类型为const Widget&

demo2:

cpp
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
decltype(auto) func(T& t)
{
return t[i];
}

int main()
{
std::deque<int> d;
func(d); //返回的类型被正确推导为int&,如果直接auto推导则会推导为int
}

3.2 特殊情况:对于不是单纯变量名的左值表达式

对于不是单纯变量名的左值表达式(能出现在等号左边的都是左值表达式,包括可以被赋值的变量),decltype推导的结果总是引用类型。

demo:

cpp
1
2
3
int x = 0; 
//decltype(x)的类型是int
//decltype((x))的类型则是int&,因为(x)被视为不是单纯变量的左值表达式

因此有些情况会引发一些未定义行为的错误

情况1:

因为(i)作为左值表达式会被推导为int&返回了临时变量的引用,导致错误。

cpp
1
2
3
4
decltype(auto) func(int i)
{
return (i);
}

情况2:

同理,++i是一个表达式,所以会被推导为int&i++不会出错,因为i++本质返回的就是变量i,推导的是int

cpp
1
2
3
4
decltype(auto) func(int i)
{
return ++i;
}

4 尾置返回类型

使用场景: 一般函数返回值的类型需要在函数声明时指定,而使用尾置返回类型可以根据函数使用时的形参来推断返回值类型。这里的auto是告诉函数,返回值类型将由后续推导决定。

语法:

cpp
1
2
3
4
5
6
7
8
9
10
11
//基本用法
auto function_name(parameters) -> 返回的类型 {
// 函数体
}

//常用用法
template<typename T>
auto function_name(T& t) -> decltype(t[i]) //假设形参是一个容器
{
return t[i];
}

这里demo只用auto做返回值会出现什么问题?
1、从2.2可知,这里的auto做返回值实际是用的模板推导的规则,如果传入花括号实参(统一初始化),则会报错。
2、如果传入的容器是std::deque<int> d;d[i]返回的数据类型则是int&,而从2.1可知,auto会把他推导为int,而这个int是拷贝的一个临时对象,是右值,无法被赋值。这时如果这样调用:function_name(d) = 5,就会出问题。