本节内容将会讲解 auto 类型推导。这样可以更直观地理解它们的关系和实际应用。
auto 类型推导示例
现在,我们来看几个使用 auto 的变量声明:
int main() {
auto x = 10; // x 的类型被推导为 int
auto y = 5.5; // y 的类型被推导为 double
auto z = "Hello"; // z 的类型被推导为 const char*
std::cout << x << std::endl;
std::cout << y << std::endl;
std::cout << z << std::endl;
return 0;
}
在这个例子中,编译器会根据初始化表达式的类型来推导 auto 声明的变量的类型。
对比分析
变量声明
在 auto 类型推导中,我们声明变量时使用 auto 关键字,而在模板类型推导中,我们通过模板函数参数来推导类型。
auto 类型推导:
auto x = 10; // x 的类型是 int
auto y = 5.5; // y 的类型是 double
auto z = "Hello"; // z 的类型是 const char*
模板类型推导:
template<typename T>
void print(T value) {
// 函数体
}
int a = 10;
print(a); // 推导 T 为 int
复合类型
我们可以通过一个更复杂的例子来展示如何处理复合类型,如指针和引用。
auto 类型推导:
int a = 10;
const int& ra = a;
auto b = ra; // b 的类型被推导为 int(ra 是 const int&,但 b 是 int)
const auto& rb = a; // rb 的类型被推导为 const int&
模板类型推导:
template<typename T>
void printValue(T value) {
std::cout << value << std::endl;
}
int a = 10;
const int& ra = a;
printValue(ra); // 推导 T 为 int(传递的参数类型是 const int&,但模板类型是 int)
template<typename T>
void printReference(const T& value) {
std::cout << value << std::endl;
}
printReference(a); // 推导 T 为 int(传递的参数类型是 int,模板类型是 const int&)
在这两个例子中,我们可以看到 auto 和模板类型推导在处理引用类型时的相似之处。
总结
通过这两个例子的对比,我们可以看到:
- ? auto 类型推导和模板类型推导在推导变量和参数的类型时有很多相似之处。
- ? 在 auto 类型推导中,编译器会根据变量的初始化表达式来推导类型。
- ? 在模板类型推导中,编译器会根据传递给模板函数的参数类型来推导模板参数的类型。
理解这两种类型推导机制,可以帮助我们在编写代码时更好地利用 C++ 的类型推导特性,使代码更简洁、更易维护。
auto 类型推导的三种情况
和模版类型推导类似,auto 类型推导同样有下面三种情况:
- ? 情况1:类型说明符是指针或引用,但不是万能引用。
- ? 情况2:类型说明符是万能引用。
- ? 情况3:类型说明符既不是指针也不是引用。
接下来通过具体的例子来详细讲解这三种类型推导情况。
情况 1:类型说明符是指针或引用,但不是万能引用
同样地,在 auto 类型推导中,如果类型说明符是普通引用,编译器会根据初始化表达式推导出具体类型。
示例:
int number = 42;
const int constNumber = number;
auto& refNumber = number; // refNumber 类型为 int&
auto& refConstNumber = constNumber; // refConstNumber 类型为 const int&
auto& refLiteral = 42; // 错误:不能绑定非常量左值引用到右值
情况 2:类型说明符是万能引用
在 auto 类型推导中,如果类型说明符是万能引用(即 auto&&),编译器同样会根据值类别推导出具体类型。
示例:
int number = 42;
const int constNumber = number;
auto&& universalRefNumber = number; // universalRefNumber 类型为 int&
auto&& universalRefConstNumber = constNumber; // universalRefConstNumber 类型为 const int&
auto&& universalRefLiteral = 42; // universalRefLiteral 类型为 int&&
- 1. auto&& universalRefNumber = number; // universalRefNumber 类型为 int&
为什么在这种情况下类型是 int& 而不是 int&& ?
auto&& 可以被称为万能引用,它的类型推导规则是基于传递给它的表达式的值类别(value category)。根据值类别,编译器会推导出不同的类型:
- ? 如果表达式是左值,则 auto 被推导为左值引用类型。
- ? 如果表达式是右值,则 auto 被推导为非引用类型。
number 是一个左值。当 auto&& 遇到左值时,编译器会将 auto 推导为左值引用类型。因此,auto&& universalRefNumber = number; 中的 auto 被推导为 int&。最终类型为 int&。
- 1. auto&& universalRefConstNumber = constNumber;
constNumber 也是一个左值。当 auto&& 遇到左值时,编译器会将 auto 推导为左值引用类型。因此,auto&& universalRefConstNumber = constNumber; 中的 auto 被推导为 const int&。最终类型为 const int&。
- 1. auto&& universalRefLiteral = 42;
42 是一个右值。当 auto&& 遇到右值时,编译器会将 auto 推导为非引用类型。因此,auto&& universalRefLiteral = 42; 中的 auto 被推导为 int。最终类型为 int&&。
- 1. 推导规则总结
- 1. 左值(有名字的变量,如 number 和 constNumber):
- ? auto&& 会推导为左值引用类型(即 int& 或 const int&)。
- ? 例子:auto&& universalRefNumber = number;,类型为 int&。
- 2. 右值(字面值或临时对象,如 42):
- ? auto&& 会推导为右值引用类型(即 int&&)。
- ? 例子:auto&& universalRefLiteral = 42;,类型为 int&&。
通过这些例子和解释,我们可以清楚地看到 auto&& 的类型推导规则。理解这些规则对于掌握 C++ 中的类型推导机制以及编写泛型代码非常重要。
情况 3:类型说明符既不是指针也不是引用
同样地,在 auto 类型推导中,如果类型说明符既不是指针也不是引用,编译器会根据初始化表达式推导出具体类型。
示例:
int number = 42;
const int constNumber = number;
auto varNumber = number; // varNumber 类型为 int
auto varConstNumber = constNumber; // varConstNumber 类型为 int
auto varLiteral = 42; // varLiteral 类型为 int
总结
auto 类型推导与模板类型推导相同。通过以上例子,我们可以看到 auto 类型推导在处理指针、引用和普通类型时的具体规则。理解这三种情况有助于我们更好地掌握 C++ 中的类型推导机制,提高代码的可读性和可维护性。
注意
下面讲解使用 auto 类型推导时要注意的点。
在 C++98 中,可以用以下两种方式初始化变量:
int number1 = 10; // 使用赋值初始化
int number2(10); // 使用构造函数初始化
在 C++11 中,引入了统一初始化语法,提供了新的初始化方式:
int number3 = { 10 }; // 使用赋值初始化和列表初始化相结合
int number4{ 10 }; // 使用列表初始化
- ? C++98 和 C++11 的传统初始化方法都能初始化一个值为 10 的 int 变量。
- ? C++11 的列表初始化提供了一种更加一致和安全的初始化方式,避免了传统方法中的一些潜在问题。
使用 auto 进行变量声明时,类型推导结果不同:
auto value1 = 27; // 类型是 int
auto value2(27); // 类型是 int
auto value3 = { 27 }; // 类型是 std::initializer_list<int>
auto value4{ 27 }; // 类型是 std::initializer_list<int>
当使用大括号初始化器时,auto 推导的类型为 std::initializer_list。若大括号中包含不同类型的值,类型推导将失败:
auto value5 = { 1, 2, 3.0 }; // 错误,无法推导出 std::initializer_list<T> 的 T
- ? 为什么 auto 和模板类型推导不同?
auto 类型推导对大括号初始化器有特殊规则,而模板类型推导没有。这意味着使用 auto 声明变量并用大括号初始化时,推导出的类型将始终是 std::initializer_list。这在使用统一初始化时需要特别注意,以避免意外声明 std::initializer_list 变量。
- ? C++14 的进一步影响
在 C++14 中,auto 可以用来指示函数的返回类型应被推导,或用于 lambda 表达式的参数声明。然而,这些情况下的 auto 类型推导采用的是模板类型推导规则,而不是 auto 类型推导规则。因此,返回大括号初始化器的 auto 返回类型函数将无法编译:
auto createInitList() {
return { 1, 2, 3 }; // 错误:无法推导 { 1, 2, 3 } 的类型
}
同样,当 auto 用于 C++14 lambda 的参数类型说明时也是如此:
std::vector<int> vec;
auto resetVec = [&vec](const auto& newValue) { vec = newValue; }; // C++14
resetVec({ 1, 2, 3 }); // 错误!无法推导 { 1, 2, 3 } 的类型
总结
总结来说,auto 和模板类型推导的唯一真正区别在于 auto 假设大括号初始化器表示 std::initializer_list。理解这一点有助于避免编程中的陷阱,使得代码更健壮和可维护。