自动类型推导
自动类型推导
苏丙榅在C++11中增加了很多新的特性,比如可以使用auto自动推导变量的类型,还能够结合decltype来表示函数的返回值。使用新的特性可以让我们写出更加简洁,更加现代的代码。
1. auto
在C++11之前auto和static是对应的,表示变量是自动存储的,但是非static的局部变量默认都是自动存储的,因此这个关键字变得非常鸡肋,在C++11中他们赋予了新的含义,使用这个关键字能够像别的语言一样自动推导出变量的实际类型。
1.1 推导规则
C++11中auto并不代表一种实际的数据类型,只是一个类型声明的 “占位符”,auto并不是万能的在任意场景下都能够推导出变量的实际类型,使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型。
使用语法如下:
1 | auto 变量名 = 变量值; |
根据上述语法,来列举一些简单的例子:
1 | auto x = 3.14; // x 是浮点型 double |
不仅如此,auto还可以和指针、引用结合起来使用也可以带上const、volatile限定符,在不同的场景下有对应的推导规则,规则内容如下:
- 当变量不是指针或者引用类型时,推导的结果中不会保留const、volatile关键字
- 当变量是指针或者引用类型时,推导的结果中会保留const、volatile关键字
先来看一组变量带指针和引用并使用auto进行类型推导的例子:
1 | int temp = 110; |
- 变量
a
的数据类型为int*
,因此auto关键字被推导为int类型
- 变量
b
的数据类型为int*
,因此auto关键字被推导为int*
类型 - 变量
c
的数据类型为int&
,因此auto关键字被推导为int类型
- 变量
d
的数据类型为int
,因此auto关键字被推导为int
类型
在来看一组带const限定的变量,使用auto进行类型推导的例子:
1 | int tmp = 250; |
- 变量
a1
的数据类型为const int
,因此auto关键字被推导为int
类型 - 变量
a2
的数据类型为int
,但是a2没有声明为指针或引用因此 const属性被去掉,auto
被推导为int
- 变量
a3
的数据类型为const int&
,a3被声明为引用因此 const属性被保留,auto关键字被推导为int
类型 - 变量
a4
的数据类型为const int&
,a4被声明为引用因此 const属性被保留,auto关键字被推导为const int
类型
1.2 auto的限制
auto关键字并不是万能的,在以下这些场景中是不能完成类型推导的:
不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。
1
2
3
4int func(auto a, auto b) // error
{
cout << "a: " << a <<", b: " << b << endl;
}不能用于类的非静态成员变量的初始化
1
2
3
4
5
6class Test
{
auto v1 = 0; // error
static auto v2 = 0; // error,类的静态非常量成员不允许在类内部直接初始化
static const auto v3 = 10; // ok
}不能使用auto关键字定义数组
1
2
3
4
5
6
7int 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无法定义数组
}无法使用auto推导出模板参数
1
2
3
4
5
6
7
8
9template <typename T>
struct Test{}
int func()
{
Test<double> t;
Test<auto> t1 = t; // error, 无法推导出模板类型
return 0;
}
1.3 auto的应用
了解了auto的限制之后,我们就可以避开这些场景快乐的编程了,下面列举几个比较常用的场景:
用于STL的容器遍历。
在C++11之前,定义了一个stl容器之后,遍历的时候常常会写出这样的代码:
1
2
3
4
5
6
7
8
9
10
11
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
int main()
{
map<int, string> person;
// 代码简化
for (auto it = person.begin(); it != person.end(); ++it)
{
// do something
}
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
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
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;
}
2. decltype
在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用C++11提供的decltype关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型,语法格式如下:
1 | decltype (表达式) |
decltype 是“declare type”的缩写,意思是“声明类型”。decltype的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子:
1 | int a = 10; |
可以看到decltype推导的表达式可简单可复杂,在这一点上auto是做不到的,auto只能推导已初始化的变量类型。
2.1 推导规则
通过上面的例子我们初步感受了一下 decltype 的用法,但不要认为 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
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;
decltype(y) b = x;
decltype(Test::value) c = 0;
Test t;
decltype(t.text) d = "hello, world";
return 0;
}- 变量
a
被推导为int
类型 - 变量
b
被推导为const int &
类型 - 变量
c
被推导为const int
类型 - 变量
d
被推导为string
类型
- 变量
表达式是函数调用,使用decltype推导出的类型和函数返回值一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class 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
//decltype类型推导
int n = 100;
decltype(func_int()) a = 0;
decltype(func_int_r()) b = n;
decltype(func_int_rr()) c = 0;
decltype(func_cint()) d = 0;
decltype(func_cint_r()) e = n;
decltype(func_cint_rr()) f = 0;
decltype(func_ctest()) g = Test();- 变量
a
被推导为int
类型 - 变量
b
被推导为int&
类型 - 变量
c
被推导为int&&
类型 - 变量
d
被推导为int
类型 - 变量
e
被推导为const int &
类型 - 变量
f
被推导为const int &&
类型 - 变量
g
被推导为const Test
类型
函数 func_cint() 返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据),
对于纯右值而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略掉这两个限定符
,因此推导出的变量d的类型为 int 而不是 const int。- 变量
表达式是一个左值,或者被括号
( )
包围,使用 decltype推导出的是表达式类型的引用(如果有const、volatile限定符不能忽略)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using namespace std;
class Test
{
public:
int num;
};
int main() {
const Test obj;
//带有括号的表达式
decltype(obj.num) a = 0;
decltype((obj.num)) b = a;
//加法表达式
int n = 0, m = 0;
decltype(n + m) c = 0;
decltype(n = n + m) d = n;
return 0;
}obj.num
为类的成员访问表达式,符合场景1,因此a
的类型为int
obj.num
带有括号,符合场景3,因此b
的类型为const int&
。n+m
得到一个右值,符合场景1,因此c
的类型为int
n=n+m
得到一个左值 n,符合场景3,因此d
的类型为int&
2.2 decltype的应用
关于decltype的应用多出现在泛型编程中。比如我们编写一个类模板,在里边添加遍历容器的函数,操作如下:
1 |
|
在程序的第17行出了问题,关于迭代器变量一共有两种类型:只读(T::const_iterator)
和读写(T::iterator)
,有了decltype就可以完美的解决这个问题了,当 T 是一个 非 const 容器得到一个 T::iterator,当 T 是一个 const 容器时就会得到一个 T::const_iterator
。
1 |
|
decltype(T().begin())
这种写法在vs2017/vs2019下测试可用完美运行。
3. 返回类型后置
在泛型编程中,可能需要通过参数的运算来得到返回值的类型,比如下面这个场景:
1 |
|
关于返回值,从上面的代码可以推断出和表达式 t+u
的结果类型是一样的,因此可以通过通过decltype进行推导,关于模板函数的参数t
和u
可以通过实参自动推导出来,因此在程序中就也可以不写。虽然通过上述方式问题被解决了,但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的。
因此如果要想解决这个问题就得直接在 add
函数身上做文章,先来看第一种写法:
1 | template <typename T, typename U> |
当我们在编译器中将这几行代码改出来后就直接报错了,因此decltype中的 t 和 u 都是函数参数,直接这样写相当于变量还没有定义就直接用上了,这时候变量还不存在,有点心急了。
在C++11中增加了返回类型后置语法,说明白一点就是将decltype和auto结合起来完成返回类型的推导
。其语法格式如下:
1 | // 符号 -> 后边跟随的是函数返回值的类型 |
通过对上述返回类型后置语法代码的分析,得到结论:auto 会追踪 decltype() 推导出的类型
,因此上边的add()
函数可以做如下的修改:
1 |
|
为了进一步说明这个语法,我们再看一个例子:
1 |
|
在这个例子中,通过decltype结合返回值后置语法很容易推导出来 test(t)
函数可能出现的返回值类型,并将其作用到了函数myFunc()
上。
1 | // 输出结果 |