C++11 引入的自动类型推导是一项非常实用的特性,它可以让编译器根据初始化表达式自动推断变量的类型,从而简化代码编写,提高代码的可读性和可维护性。下面从auto和decltype两个关键字来详细介绍这一特性。
1. auto关键字
- 基本用法
auto关键字允许编译器在编译时根据变量的初始化表达式自动推断其类型。使用auto可以避免显式指定复杂的类型,使代码更加简洁。使用auto声明的变量必须要进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换为真正的类型。
#include <iostream>
#include <vector>
int main() {
// 自动推导为 int 类型
auto num = 10;
// 自动推导为 double 类型
auto d = 3.14;
std::vector<int> vec = {1, 2, 3, 4, 5};
// 自动推导为 std::vector<int>::iterator 类型
auto it = vec.begin();
std::cout << "num type: " << typeid(num).name() << std::endl;
std::cout << "d type: " << typeid(d).name() << std::endl;
std::cout << "it type: " << typeid(it).name() << std::endl;
return 0;
}
在上述代码中,num被初始化为整数10,编译器自动将其类型推导为int;d被初始化为浮点数3.14,推导为double;it是std::vector<int>的迭代器,使用auto避免了显式写出复杂的迭代器类型。
- 与指针、引用和限定符结合:
当变量不是指针或引用类型时,推导结果中不会保留const、volatile关键字。当变量是指针或引用类型时,推导结果中会保留const、volatile关键字。如下:
int temp = 110;
const int constTemp = 200;
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 e = constTemp; // e 是 const int 类型,auto 被推导为 int
auto f = e; // f 是 int 类型,auto 被推导为 int
const auto &g = constTemp; // g 是 const int& 类型,auto 被推导为 int
auto &h = g; // h 是 const int& 类型,auto 被推导为 const int
- 用于范围for循环
在C++11之前,定义了一个stl容器之后,遍历的时候常常会写出这样的代码:
#include <map>
int main()
{
map<int, string> person;
map<int, string>::iterator it = person.begin();
for (; it != person.end(); ++it)
{
// do something
}
return 0;
}
可以看到在定义迭代器变量 it 的时候代码是很长的,写起来就很麻烦,然而在范围for循环中,auto可以方便地处理容器中的元素。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto element : vec) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
这里auto自动推导element的类型为int,简化了循环的书写。
- 用于泛型编程
在使用模板的时候,很多情况下我们不知道变量应该定义为什么类型,比如下面的代码:
#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的返回值类型,具体代码如下:
#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;
}
注意事项
- 必须初始化:使用auto声明变量时,必须进行初始化,因为编译器需要根据初始化表达式来推断类型。
- 类型可能会有衰减:auto推导的类型可能会发生一些类型衰减,例如数组会被推导为指针类型。
2.auto使用限制
- 不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。
int func(auto a, auto b) // error
{
cout << "a: " << a <<", b: " << b << endl;
}
- 不能用于类的非静态成员变量的初始化。
class Test
{
auto v1 = 0; // error
static auto v2 = 0; // error,类的静态非常量成员不允许在类内部直接初始化
static const auto v3 = 10; // ok
}
- 不能使用auto关键字定义数组。
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无法定义数组
}
- 无法使用auto推导出模板参数。
template <typename T>
struct Test{}
int func()
{
Test<double> t;
Test<auto> t1 = t; // error, 无法推导出模板类型
return 0;
}
3.decltype关键字
- 基本用法
decltype用于在编译时推导表达式的类型,它可以根据给定的表达式返回其确切的类型,而不实际计算表达式的值。
#include <iostream>
int func() { return 10; }
int main() {
int x = 5;
// 推导为 int 类型
decltype(x) y;
// 推导为 int 类型
decltype(func()) z;
std::cout << "y type: " << typeid(y).name() << std::endl;
std::cout << "z type: " << typeid(z).name() << std::endl;
return 0;
}
在上述代码中,decltype(x)推导y的类型为int,decltype(func())推导z的类型也为int。
- 用于模板编程
decltype在模板编程中非常有用,可以根据模板参数的表达式来推导返回类型。如下所示返回值类型后置:
#include <iostream>
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
int main() {
int x = 5;
double y = 3.14;
auto result = add(x, y);
std::cout << "result type: " << typeid(result).name() << std::endl;
return 0;
}
这里decltype(a + b)用于推导add函数的返回类型,使得函数可以处理不同类型的参数相加。
4.decltype关键字推导规则
- 表达式为普通变量、普通表达式或类表达式:推导结果与表达式的类型一致。例如:
#include <iostream>
#include <string>
class Test
{
public:
std::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 是 std::string 类型
return 0;
}
- 表达式是函数调用:推导结果与函数返回值的类型一致。需要注意的是,对于纯右值(表达式执行结束后不再存在的数据),只有类类型可以携带const、volatile限定符,其他情况需要忽略这两个限定符。例如:
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 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 类型
- 表达式是一个左值,或者被括号()包围:推导结果是表达式类型的引用(如果有const、volatile限定符不能忽略)。例如:
#include <iostream>
#include <vector>
class Test
{
public:
int num;
};
int main() {
const Test obj;
decltype(obj.num) a = 0; // a 是 int 类型
decltype((obj.num)) b = a; // b 是 const int& 类型
int n = 0, m = 0;
decltype(n + m) c = 0; // c 是 int 类型
decltype(n = n + m) d = n; // d 是 int& 类型
return 0;
}