自动类型推断(Automatic Type Inference)是指编程语言在编译时自动根据上下文推导变量的类型,而无需显式指定变量的类型。
1. auto 定义变量
在C++ 11中,自动类型推断是通过关键字auto关键字
来实现的。auto类型并不代表一种实际的数据类型,它只是一个类型声明的“占位符”,auto类型变量是在编译时根据给定的初始值推导出变量的类型。
C++中,变量分为局部变量、全局变量、类成员变量,下面我们使用auto关键字声明定义这几种类型变量的方面,来讨论自动类型推导的用法。
1.1 局部变量
大多数情况下,我们都是使用auto声明定义局部变量的,形式如下所示:
auto x = 42; // int x = 42;
auto name = "John"; // const char *name = "John"
auto pi = 3.14f; // float pi = 3.14f;
除上例之外,auto类型还可以和指针、引用结合使用,也可以带上const 、volatile限定符。推导规则如下:
int var = 110;
auto *a = &var; // int *a = &var;
auto b = &var; // int *b = &var;
auto &c = var; // int &c = var;
const auto a1 = var; // const int a1 = var;
auto a2 = a1; // int a2 = a1;
// => 由于a2是非指针/引用类型、因此const属性被删除
const auto &a3 = var; // const int &a3 = var;
// => 由于a3是引用类型、因此const属性被保留
auto &4 = a3; // const int &a4 = a3;
// => 由于a4是引用类型、因此const属性被保留
结论:
- 当变量是指针或引用类型时,auto推导结果会保留const、volatile属性
- 当变量不是指针或引用类型时,auto推导结果中不会保留const、volatile属性
在这里,对字符串类型变量需要说明一下,因为使用字符串初始化的auto类型变量,通常会被推导成const char *类型。但是有时候我们还想让auto类型推导成std::string类型,那么只需要初始化的字符串的末尾追加“
s”
符号标注即可,如下:
void func() {
auto c_str = "Hello, world"; // const char *
auto str = "Hello, world"s; // std::string
...
使用的auto声明定义的局部变量,经常被使用在基于范围的 for 循环中遍历容器、数组等当中的元素,如下:
int nums[] = {1, 2, 3, 4, 5};
for (int num : nums) {
std::cout << num << ' ';
}
关于此用法的详细说明,请参考【C++ 11】(二)基于范围的for循环
1.2 全局变量
C++中,全局变量分为两种:外部全局变量和静态局部变量。
1.2.1 外部全局变量
C++中,为了避免在不同的源文件中出现重复定义的问题,外部全局变量会被声明为extern,然后实现在另一个文件中进行定义实现。
在C++ 11中,外部全局变量是不能被定义成auto类型的。如下定义会在编译期间报错的。
extern auto g_variable; // 编译时会报错
...
auto g_variable = true;
1.2.2 静态全局变量
我们再说说静态全局变量,它是指在一个文件中定义的全局变量,是由static定义的变量。它的生命周期会持续到程序的结束,并且只能在当前文件中访问。
在C++ 11中,静态全局变量是可以定义成auto类型的。在编译期间,它的类型会根据给定的初始值推导出真正的类型。如下示例:
static auto s_variable = 3.1415f; // 推导成 float 类型
int main(void) {
std:cout << "variable is " << s_variable << std::endl;
return 0;
}
1.3 类成员变量
C++中,类成员变量分为两种:静态成员变量和非静态成员变量。
1.3.1 静态成员变量
由于类的静态成员变量在声明定义时,不能给定一个初始值,无法推导出类型,所以它无法被定义成auto类型。如下示例:
class Sample {
public:
static auto _value; // 编译时报错
//static auto _value = 3; // 编译时报错,不支持此写法
};
auto Sample::_value = 10;
1.3.2 非静态成员变量
对于非静态成员变量,由于能够给定初始值,我本认为是可以使用auto关键来进行声明定义的,但是事实上非静态成员变量被声明auto类型后,编译阶段就会报错。所以所以它无法被定义成auto类型。如下示例:
class Sample {
public:
//int _value = 10; // 编译成功
auto _value = 10; // 编译时报错
};
1.3.3 静态常量
在类中,虽然无论是静态成员变量或是非静态成员变量都不能被定义成auto类型,但是类的静态常量确是可以使用auto来声明定义的。需要注意,一般情况下,使用const关键字只能声明定义 int 类型的常量,若想定义声明其它类型的静态常量时,可使用C++ 11新引入的 constexpr 关键字来定义声明。如下所示:
class Sample {
public:
static const auto _value = 1; // 编译成功
//static const auto _pi = 3.14; // 编译报错
static constexpr auto _pi = 3.14; // 编译成功
};
2. auto 定义函数
在C++ 11中,无论是函数的返回值还是函数参数都是不能被定义成auto类型的,但从C++ 14后就支持了这种特性。
2.1 函数返回值
从C++ 14后,对于任何函数及类中的成员函数的返回值都可定义成auto类型,但是对类中的虚函数(virtual定义)的返回值是不能够使用auto类型的。
在C++ 14中,关于函数auto类型返回值的推导,我们经常会遇到以下几种情况:
- 返回数据的类型一致
#include <iostream>
template <typename T>
auto max(const T& a, const T& b) {
return (a < b) ? b : a;
}
int main(void) {
std::cout << max(4, 5) << std::endl;
return 0;
}
编译成功,输出结果:
5
- 返回数据的类型不一致
#include <iostream>
template <typename T, typename U>
auto max(const T& a, const U& b) {
return (a < b) ? b : a;
}
int main(void) {
//std::cout << max("7.2", 5) << std::endl; // 编译时会报错
std::cout << max(7.2, 5) << std::endl;
return 0;
}
编译成功,输出结果:
7.2
但是,我发现一个很奇怪的问题,如果将上例函数中的三目运算符修改成使用 if 语句检查判断,用多个return语句返回不同类型的数据时,就会发生编译错误。如下:
#include <iostream>
template <typename T, typename U>
auto max(const T& a, const U& b) {
if (a < b)
return b;
else
return a; // 编译时会报错
}
int main(void) {
std::cout << max(7.2, 5) << std::endl;
return 0;
}
如何解决这种问题呢?可以直接使用三目运算符解决,也可以使用“后置返回值类型”声明返回值的类型。如下:
#include <iostream>
template <typename T, typename U>
auto max(const T& a, const U& b) -> decltype(a + b) { // 后置返回值类型声明
if (a < b)
return b;
else
return a;
}
int main(void) {
std::cout << max(7.2, 5) << std::endl;
return 0;
}
编译成功,输出结果:
7.2
2.2 函数参数
在C++14中引入的“泛型Lambda表达式”,它可以将参数声明定义成auto类型,实现函数参数类型的自动推导。如下示例:
auto func = [](auto arg1, auto arg2) {
// Lambda函数体
};
3. auto 使用说明
3.1 用途
3.1.1 简化变量类型的声明
使用auto可以省去手动指定变量类型的麻烦。编译器会根据初始化表达式的类型来推断出变量的类型,使代码更简洁。
auto num = 10;
auto str = "Hello";
auto res = calculate_result();
特别适用于处理复杂或模板化的类型,因为它消除了在代码中显式声明冗长或冗余类型名称的需要。例如:
vector<vector<string>> vec;
for(vector<vector<string>>::iterator i = vec.begin();i != vec.end(); i++) {
// .... code ...
}
使用auto声明后:
vector<vector<string>> vec;
for(auto i = vec.begin();i != vec.end(); i++) {
// .... code ...
}
3.1.2 支持复杂类型的推断
auto可以推断复杂的类型,例如模板类型、迭代器类型和lambda表达式类型。
std::vector<int> nums = {1, 2, 3, 4, 5};
auto it = nums.begin(); // 推导为 std::vector<int>::iterator
auto add = [](int a, int b) { return a + b; }; // 推导为 lambda表达式类型
auto res = add(3, 5); // res的类型将推导为lambda表达式的返回类型
3.1.3 提高代码的灵活性和重构能力
使用auto可以避免重复修改变量声明的类型,尤其是在复杂的类型改变时。这样可以提高代码的重构能力和可维护性。
auto data = fetch_data_from_network(); // 假设返回类型为 std::vector<int>
// ... 执行一些操作
auto newData = modify_data(data); // 修改后的数据的类型不必手动更改
尤其在使用auto定义泛型的时候,大大提高了代码灵活性和使用上的复杂性,例如不使用auto的情况下,定义一个模板:
class A {
public:
static int get() { return 0x10; }
};
class B {
public:
static double get() { return 3.14f; }
};
template <class X, typename Y>
void func(void) {
Y v = X::get();
cout << "value: " << v << "\n";
};
int main() {
func(A, int);
func(B, double);
return 0;
}
在使用auto的情况下,代码可以修改成以下样式:
class A {
public:
static int get() { return 0x10; }
};
class B {
public:
static double get() { return 3.14f; }
};
template <class X> // 此处不用另外追加模板参数
void func(void) {
auto v = X::get();
cout << "value: " << v << "\n";
};
int main() {
func(A);
func(B);
return 0;
}
3.2 制限
在使用C++ 11 auto关键字声明定义的时修,也是有制限的。
- 不能用于作为函数参数的声明
int func(auto a, auto b) { // error
// .... code ...
}
- 不能用于类中静态常量之外的变量的声明
class A {
auto v1 = 0; // error
static auto v2 = 0; // error
static const auto v3 = 10; // ok
}
- 不能用于定义数组
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无法定义数组
}
- 不能用于推导出模板参数
template <typename T>
struct A{}
int func() {
A<double> t;
A<auto> t1 = t; // error, 无法推导出模板类型
return 0;
}
总而言之,auto是C++ 11引入的功能之一,它可以帮助简化代码、减少冗余,并提高编写和维护代码的效率。