自动类型推导

在 C++11 中增加了很多新的特性,可以使用 auto 自动推导变量的类型,还能够结合 decltype 来表示函数的返回值。
使用新的特性可以让我们写出更加简洁,更加现代的代码。
在 C++11 之前 autostatic 是对应的,表示变量是自动存储的,但是非 static 的局部变量默认都是自动存储的,因此这个关键字变得非常鸡肋,在 C++11 中赋予了新的含义,使用这个关键字能够像别的语言一样自动推导出变量的实际类型。

auto 推导规则

C++11 中 auto 并不代表一种实际的数据类型,只是一个类型声明的 “占位符”,auto 并不是在任意场景下都能够推导出变量的实际类型。使用auto 声明的变量必须要进行初始化,让编译器推导出它的实际类型,在编译时将 auto 占位符替换为真正的类型。使用语法如下:

1
2
3
4
5
auto x = 3.14;    // x 是浮点型 double
auto y = 520; // y 是整形 int
auto z = 'a'; // z 是字符型 char
auto nb; // error,变量必须要初始化
auto double nbl; // 语法错误,不能修改数据类型

不仅如此,auto 还可以和指针、引用结合起来使用,也可以带上 constvolatile 限定符。
在不同的场景下有对应的推导规则,规则内容如下:
(1)当变量不是指针或者引用类型时,推导的结果中不会保留 constvolatile 关键字。
(2)当变量是指针或者引用类型时,推导的结果中会保留 constvolatile 关键字。

变量带指针和引用并使用 auto 进行类型推导的例子:

1
2
3
4
5
int temp = 110;
auto *a = &temp; // 变量 a 的数据类型为 int*,因此 auto 关键字被推导为 int 类型
auto b = &temp; // 变量 b 的数据类型为 int*,因此 auto 关键字被推导为 int* 类型
auto &c = temp; // 变量 c 的数据类型为 int&,因此 auto 关键字被推导为 int 类型
auto d = temp; // 变量 d 的数据类型为 int,因此 auto 关键字被推导为 int 类型

const 限定的变量,使用 auto 进行类型推导的例子:

1
2
3
4
5
int tmp = 250;        
const auto a1 = tmp; // 变量 a1 的数据类型为 const int,因此 auto 关键字被推导为 int 类型
auto a2 = a1; // 变量 a2 的数据类型为 int,a2 没有声明为指针或引用,因此 const 属性被去掉,auto 被推导为 int
const auto &a3 = tmp; // 变量 a3 的数据类型为 const int&,a3 被声明为引用,因此 const 属性被保留,auto 关键字被推导为 int 类型
auto &a4 = a3; // 变量 a4 的数据类型为 const int&,a4 被声明为引用,因此 const 属性被保留,auto 关键字被推导为 const int 类型

auto 的限制

auto 关键字并不是万能的,在以下这些场景中不能完成类型推导:
(1)不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto 要求必须要给修饰的变量赋值,因此二者矛盾。

1
2
3
4
int func(auto a, auto b)  // error
{
cout << "a: " << a << ", b: " << b << endl;
}

(2)不能用于类的非静态成员变量的初始化

1
2
3
4
5
6
class Test
{
auto v1 = 0; // error
static auto v2 = 0; // error,类的静态非常量成员不允许在类内部直接初始化
static const auto v3 = 10; // ok
}

(3)不能使用 auto 关键字定义数组

1
2
3
4
5
6
7
int func()
{
int array[] = {1,2,3,4,5}; // 定义数组
auto t1 = array; // ok,t1 被推导为 int* 类型
auto t2[] = array; // error,auto 无法定义数组
auto t3[] = {1,2,3,4,5};; // error,auto 无法定义数组
}

(4)无法使用 auto 推导出模板参数

1
2
3
4
5
6
7
8
9
template <typename T>
struct Test{};

int func()
{
Test<double> t;
Test<auto> t1 = t; // error,无法推导出模板类型
return 0;
}

auto 的应用

了解 auto 的限制之后,就可以避开这些场景编程,下面列举几个比较常用的场景:
(1)用于 STL 的容器遍历
在 C++11 之前,定义了一个 STL 容器之后,遍历的时候常会写出这样的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <map>
using namespace std;

int main()
{
map<int, string> person;
map<int, string>::iterator it = person.begin();
for (; it != person.end(); ++it)
{
// do something
}

return 0;
}

定义迭代器变量 it 的时候代码是很长的,写起来很麻烦,使用了 auto 之后,就变得简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <map>
using namespace std;

int main()
{
map<int, string> person;
// 代码简化
for (auto it = person.begin(); it != person.end(); ++it)
{
// do something
}

return 0;
}

(2)用于泛型编程
在使用模板的时候,很多情况下不知道变量应该定义为什么类型,示例代码如下:

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
#include <iostream>
#include <string>
using namespace std;

class T1
{
public:
static int get()
{
return 10;
}
};

class T2
{
public:
static string get()
{
return "hello, world";
}
};

template <class A>
void func(void)
{
auto val = A::get();
cout << "val: " << val << endl;
}

int main()
{
func<T1>();
func<T2>();

return 0;
}

在这个示例中定义了泛型函数 func,在函数中调用了类 A 的静态方法 get(),这个函数的返回值是不能确定的,如果不使用 auto,就需要再定义一个模板参数,并且在外部调用时手动指定 get() 的返回值类型,示例代码如下:

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
#include <iostream>
#include <string>
using namespace std;

class T1
{
public:
static int get()
{
return 0;
}
};

class T2
{
public:
static string get()
{
return "hello, world";
}
};

template <class A, typename B> // 添加了模板参数 B
void func(void)
{
B val = A::get();
cout << "val: " << val << endl;
}

int main()
{
func<T1, int>(); // 手动指定返回值类型 -> int
func<T2, string>(); // 手动指定返回值类型 -> string

return 0;
}

decltype

在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候可以使用 C++11 提供的 decltype 关键字,它的作用是在编译器编译的时候推导出一个表达式的类型。
decltype(declare type),意思是 “声明类型”。decltype 的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。

示例代码如下:

1
2
3
4
int a = 10;
decltype(a) b = 99; // b -> int
decltype(a + 3.14) c = 52.13; // c -> double
decltype(a + b*c) d = 520.1314; // d -> double

decltype 推导的表达式可简单可复杂,auto 只能推导已初始化的变量类型。

decltype 推导规则

decltype 的背后隐藏着很多的细节,下面分三个场景讨论:
(1)表达式为普通变量/普通表达式/类表达式,在这种情况下,使用 decltype 推导出的类型和表达式的类型是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>
using namespace std;

class Test
{
public:
string text;
static const int value = 110;
};

int main()
{
int x = 99;
const int &y = x;
decltype(x) a = x; // 变量 a 被推导为 int 类型
decltype(y) b = x; // 变量 b 被推导为 const int & 类型
decltype(Test::value) c = 0; // 变量 c 被推导为 const int 类型
Test t;
decltype(t.text) d = "hello, world"; // 变量 d 被推导为 string 类型

return 0;
}

(2)表达式是函数调用,使用 decltype 推导出的类型和函数返回值一致。

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
#include <iostream>
#include <string>
using namespace std;

class Test{};

// 函数声明
int func_int(); // 返回值为 int
int& func_int_r(); // 返回值为 int&
int&& func_int_rr(); // 返回值为 int&&
const int func_cint(); // 返回值为 const int
const int& func_cint_r(); // 返回值为 const int&
const int&& func_cint_rr(); // 返回值为 const int&&
const Test func_ctest(); // 返回值为 const Test

int main()
{
// decltype类型推导
int n = 100;
decltype(func_int()) a = 0; // 变量 a 被推导为 int 类型
decltype(func_int_r()) b = n; // 变量 b 被推导为 int& 类型
decltype(func_int_rr()) c = 0; // 变量 c 被推导为 int&& 类型
decltype(func_cint()) d = 0; // 变量 d 被推导为 int 类型
decltype(func_cint_r()) e = n; // 变量 e 被推导为 const int & 类型
decltype(func_cint_rr()) f = 0; // 变量 f 被推导为 const int && 类型
decltype(func_ctest()) g = Test(); // 变量 g 被推导为 const Test 类型

return 0;
}

函数 func_cint() 返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据)。对于纯右值而言,只有类类型可以携带 constvolatile 限定符,除此之外需要忽略掉这两个限定符,因此推导出的变量 d 的类型为 int 而不是 const int
(3)表达式是一个左值,或者被括号 () 包围,使用 decltype 推导出的是表达式类型的引用(如果有 constvolatile 限定符不能忽略)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class Test
{
public:
Test() {}
int num;
};

int main()
{
const Test obj;
// 带有括号的表达式
decltype(obj.num) a = 0; // obj.num 为类的成员访问表达式,符合场景 1,因此 a 的类型为 int
decltype((obj.num)) b = a; // obj.num 带有括号,符合场景 3,因此 b 的类型为 const int&
// 加法表达式
int n = 0, m = 0;
decltype(n + m) c = 0; // n + m 得到一个右值,符合场景 1,因此 c 的类型为 int
decltype(n = n + m) d = n; // n = n + m 得到一个左值 n,符合场景 3,因此 d 的类型为 int&

return 0;
}

decltype 的应用

decltype 的应用常常出现在泛型编程中。比如编写一个类模板,在里面添加遍历容器的函数,操作如下:

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
#include <iostream>
#include <list>
using namespace std;

template <typename T>
class Container
{
public:
void func(T& c)
{
for (m_it = c.begin(); m_it != c.end(); ++m_it)
{
cout << *m_it << " ";
}
cout << endl;
}
private:
decltype(T().begin()) m_it; // 这里不能确定迭代器类型
};


int main()
{
const list<int> lst { 1, 2, 3, 4, 5 };
Container<const list<int>> obj;
obj.func(lst);

return 0;
}

返回类型后置

在泛型编程中,可能需要通过参数的运算来得到返回值的类型,比如下面这个场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

// R->返回值类型, T->参数1类型, U->参数2类型
template <typename R, typename T, typename U>
R add(T t, U u)
{
return t + u;
}

int main()
{
int x = 520;
double y = 13.14;
// auto z = add<decltype(x + y), int, double>(x, y);
auto z = add<decltype(x + y)>(x, y); // 简化后的写法
cout << "z: " << z << endl;

return 0;
}

关于返回值,从上面的代码可以推断出和表达式 t + u 的结果类型是一样的,因此可以通过 decltype 进行推导,关于模板函数的参数 tu 可以通过实参自动推导出来,因此在程序中可以不写。虽然通过上述方式问题被解决了,但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的。
因此,想解决这个问题就得直接在 add 函数上做文章,先来看第一种写法:

1
2
3
4
5
template <typename T, typename U>
decltype(t + u) add(T t, U u)
{
return t + u;
}

在编译器中将这几行代码改出来后直接报错,因为 decltype 中的 tu 都是函数参数,这样写相当于变量还没有定义就使用了,这时候变量还不存在。在 C++11 中增加了返回类型后置语法,将 decltypeauto 结合起来完成返回类型的推导。其语法格式如下:

1
2
// 符号 -> 后边跟随的是函数返回值的类型
auto func(参数1, 参数2, ...) -> decltype(参数表达式)

上述代码中,auto 会追踪 decltype() 推导出的类型,因此上边的 add() 函数可以做如下的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}

int main()
{
int x = 520;
double y = 13.14;
// auto z = add(int, double) (x, y);
auto z = add(x, y); // 简化后的写法
cout << "z: " << z << endl;

return 0;
}

为了进一步说明这个语法,示例代码如下:

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
#include <iostream>
using namespace std;

int& test(int &i)
{
return i;
}

double test(double &d)
{
d = d + 100;
return d;
}

template <typename T>
// 返回类型后置语法
auto myFunc(T& t) -> decltype(test(t))
{
return test(t);
}

int main()
{
int x = 520;
double y = 13.14;
// auto z = myFunc<int>(x);
auto z = myFunc(x); // 简化后的写法
cout << "z: " << z << endl;

// auto z = myFunc<double>(y);
auto z1 = myFunc(y); // 简化后的写法
cout << "z1: " << z1 << endl;

return 0;
}

参考资料

https://subingwen.cn/cpp/autotype/


自动类型推导
https://lcf163.github.io/2021/09/15/自动类型推导/
作者
乘风的小站
发布于
2021年9月15日
许可协议