Type Deduction - Auto in C++

Syntactic Sugar Introduced by C++11

在类型推断的第二节,我们正式开始学习 C++ 的自动类型推断机制。C++ 有各种各样的数据类型:intlongfloatdouble 还有表示字符串的 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

Deduction Rule of 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 类型。由于 arr1arr2 的初始化都是通过初始化列表完成的,所以 arr1 = {1, 2, 3.14} 不会编译通过,arr2 会被推导成 std::initializer_list<int> 类型,这是一个类似数组的只读类型。

在使用 auto 时,还有一点需要注意,那就是 auto 在作为函数返回值时,它的推导行为是模板类型推导而非标准的 auto 类型推导,比如:

auto func(){
	return {1, 2, 3};
}

是编译不通过的,因为模板类型推导规则是不能处理初始化列表的。auto 的这种不一致性非常奇怪,难以理解。

The Explicitly Typed Initializer Idiom