tags:
- Cpp
Type Deduction - Auto in C++
Do this at first: Type Deduction - Template Type Deduction
在类型推断的第二节,我们正式开始学习 C++ 的自动类型推断机制。C++ 有各种各样的数据类型:int
、 long
、 float
、 double
还有表示字符串的 const char*
等等。之前,每次定义一个变量的时都要带上不同的类型符,好不麻烦。在 C++11 引入了 auto
关键字来帮我们推导类型。
简单来说,我们在定义类型的时候不需要再考虑它是什么类型了,编译器会帮我们做这些。甚至你可以让它帮你推导自定义的类类型。我们用下面的代码举一些例子:
class MyClass {
public:
MyClass(int val) : i(val) {}
int i;
};
auto func(){ // return type is deduced as int type, since C++14
return 0;
}
int main() {
int a = 0;
auto b = a; // b is deduced as int type
auto c = 0; // c is deduced as int type
auto d = 3.14; // d is deduced as double type
auto obj = MyClass{10}; // obj is deduced as MyClass type
std::cout << "obj.i = " << obj.i << std::endl; // Outputs: obj.i = 10
return 0;
}
这里 auto
是一个占位符(placeholder),在编译时,编译器会将自动推导变量的类型或函数的返回类型替换掉这里的 auto
。
auto
虽然上面的例子看不出 auto
类型推断的规则,但完成 Type Deduction 的第一部分后,你就实际上已经掌握了 auto
类型推导的大部分精髓了。但有一个例外,我们留在最后学习。
之所以说你已经掌握了 auto
类型推导的大部分精髓,是因为 auto
和模板类型推导几乎是一回事。作为 C++11 为我们提供的语法糖,虽然 auto
只提供更简单的接口,其底层机制和模板类型推导是类似的。在学习 auto
时,你完全可以想象一个模板来帮助学习。例如:
auto x = expr;
你可以将其想像成:
template<typename T>
void func(T param);
func(expr)
然后推一推 T
的类型是什么,即为 auto
推得的类型。
上节,我们从 ParamType
将函数模板类型推断分为三个 cases:
ParamType
是指针或引用类型,但不是万能引用;ParamType
是万能引用;ParamType
既不是是指针也不是引用类型。同样,当我们使用 auto
时,我们可以继承上节课的推导方法,前面的 ParamType
就是等式左边的类型,即最终推得变量的类型,如:
// fellow the rule 3: neither prt nor ref
auto i = 10;
const auto ci = 10;
const int& a = ci; // alias
auto i2 = a;
// fellow the rule 2: universal ref type
auto&& uri1 = i;
auto&& uri2 = ci;
auto&& uri3 = 10;
// fellow the rule 1: non-universal ref or ptr type
auto& ri1 = i;
const auto& ri2 = i;
const auto* pi = &i;
// the two special cases, to be said:
const char greeting[] = "Hello, world!";
auto arr1 = greeting;
auto& arr2 = greeting;
void func();
auto func1 = func;
auto& func2 = func;
我们可以沿着上节的 rules 进行推导。答案如下:
// follow Rule 3: neither pointer nor reference
auto i = 10; // (1) auto deduced as int → i: int
const auto ci = 10; // (2) auto deduced as int → ci: const int
const int& a = ci;
auto i2 = a; // (3) auto deduced as int
// follow Rule 2: universal ref
auto&& uri1 = i; // (4) i is lvalue → uri1: int&
auto&& uri2 = ci; // (5) ci is const lvalue → uri2: const int&
auto&& uri3 = 10; // (6) 10 is rvalue → uri3: int&&
// follow Rule 1: non-universal ref or pointer
auto& ri1 = i; // (7) i: int → ri1: int&
const auto& ri2 = i; // (8) i: int → ri2: const int&
const auto* pi = &i; // (9) i: int → pi: const int*
// special cases
const char greeting[] = "Hello, world!";
auto arr1 = greeting; // (10) array decays → arr1: const char*
auto& arr2 = greeting; // (11) array bound kept → arr2: const char (&)[14]
void func();
auto func1 = func; // (12) function decays to pointer → func1: void (*)()
auto& func2 = func; // (13) reference preserves function → func2: void (&)()
嘿嘿,所以 auto
的行为和模板类型推断是一样的。但我们提到过有一点是不一样的,这就不得不提到 C++11 和 auto
一并引入的更安全的变量初始化方式——初始化列表(详见 Member Initializer List in C++ (ENG)),而推出模板的 C++98 并没有初始化列表的概念,这就造成了他们唯一的不同。
在 C++98 时代,变量的初始化主要用两种方式:
int x = 10;
int y(10);
int z = 3.14; // implicit conversion happens, code compiles
这两种初始化方式会纵容 implicit conversion 的发生,而且第二种初始化的语法和函数调用的语法很像,容易混淆。C++11 引入了更现代、更安全的初始化方式,即初始化列表,它的语法是这样的:
int x = {10};
int y{10};
int z = {3.14} // error
使用初始化列表后,编译器会进行类型检查,而且不允许 implicit conversion 的发生。这样,在类型换成 auto
后,安全性就可以得到保障,比如:
auto d = (1, 2, 3.14); // unsafe but compiles
auto arr1 = {1, 2, 3.14}; // error happens
auto arr2 = {1, 2, 3, 4}; // okay, arr3 is a read-only initializer list type
// std::initializer_list<int>
这里,我们想要初始化一个 array-like 的变量,而由于逗号运算符的关系 arr1
最后会被推导为 double
类型。由于 arr1
和 arr2
的初始化都是通过初始化列表完成的,所以 arr1 = {1, 2, 3.14}
不会编译通过,arr2
会被推导成 std::initializer_list<int>
类型,这是一个类似数组的只读类型。
在使用 auto
时,还有一点需要注意,那就是 auto
在作为函数返回值时,它的推导行为是模板类型推导而非标准的 auto
类型推导,比如:
auto func(){
return {1, 2, 3};
}
是编译不通过的,因为模板类型推导规则是不能处理初始化列表的。auto
的这种不一致性非常奇怪,难以理解。