10-8 使用 auto 关键字对对象进行类型推断
这个简单的变量定义中隐藏着微妙的冗余:
double d{ 5.0 };
在 C++ 中,所有对象都必须显式指定类型。因此我们明确将变量 d 定义为 double 类型。
然而用于初始化 d 的字面量 5.0 本身也属于 double 类型(通过字面量格式隐式确定)。
相关内容
我们在第 5.2 课——字面量中讨论过字面量类型的确定机制。
当变量及其初始化表达式需要相同类型时,我们实际上重复提供了两次类型信息。
初始化变量的类型推断
类型推断(有时也称为类型推理)是编译器根据对象初始化表达式推断对象类型的特性。定义变量时,可通过使用 auto 关键字替代变量类型来触发类型推断:
int main()
{
auto d { 5.0 }; // 5.0 is a double literal, so d will be deduced as a double
auto i { 1 + 2 }; // 1 + 2 evaluates to an int, so i will be deduced as an int
auto x { i }; // i is an int, so x will be deduced as an int
return 0;
}


首例中,由于 5.0 是双精度字面量,编译器将推断变量 d 类型为 double。次例中,表达式 1 + 2 结果为 int,故变量 i 类型为 int。第三例中,i 已被推断为 int 类型,因此 x 也将被推断为 int 类型。
警告
在 C++17 之前,auto d{ 5.0 }; 会将 d 推断为 std::initializer_list类型而非 double。此问题已在 C++17 中修复,许多编译器(如 gcc 和 Clang)已将此变更回溯移植到旧版语言标准。
若使用C++14及更早版本,且上述示例无法在编译器上编译,请改用auto进行复制初始化(auto d = 5.0)。
由于函数调用是有效表达式,即使初始化项为非void函数调用,我们仍可使用类型推断:
int add(int x, int y)
{
return x + y;
}
int main()
{
auto sum { add(5, 6) }; // add() returns an int, so sum's type will be deduced as an int
return 0;
}


add()函数返回int值,因此编译器会推断变量sum应为int类型。
字面量后缀可与类型推导结合使用以指定特定类型:
int main()
{
auto a { 1.23f }; // f suffix causes a to be deduced to float
auto b { 5u }; // u suffix causes b to be deduced to unsigned int
return 0;
}


使用类型推导的变量也可使用其他限定符/修饰符,如 const 或 constexpr:
int main()
{
int a { 5 }; // a is an int
const auto b { 5 }; // b is a const int
constexpr auto c { 5 }; // c is a constexpr int
return 0;
}


类型推导必须有推导依据
对于既无初始化器又无空初始化器的对象,类型推导将失效。当初始化表达式类型为 void(或其他不完整类型)时同样失效。因此以下写法无效:
#include <iostream>
void foo()
{
}
int main()
{
auto a; // The compiler is unable to deduce the type of a
auto b { }; // The compiler is unable to deduce the type of b
auto c { foo() }; // Invalid: c can't have type incomplete type void
return 0;
}

虽然基础数据类型的类型推断仅节省少量(甚至无)键盘输入,后续课程将展示类型复杂冗长的实例(某些情况下甚至难以推断)。此时使用auto能大幅减少输入量(并避免拼写错误)。
相关内容
指针与引用的类型推导规则更为复杂,详见12.14节——指针、引用及const的类型推导。
类型推导会从推导出的类型中移除 const 限定符
在大多数情况下,类型推导会从推导出的类型中移除 const 限定符。例如:
int main()
{
const int a { 5 }; // a has type const int
auto b { a }; // b has type int (const dropped)
return 0;
}


在上例中,变量 a 的类型为 const int,但当使用 a 作为初始化器推导变量 b 的类型时,类型推导会将其推导为 int 类型而非 const int。
若需使推导出的类型为 const,必须在定义中显式添加 const 限定符:
int main()
{
const int a { 5 }; // a has type const int
const auto b { a }; // b has type const int (const dropped but reapplied)
return 0;
}
在此示例中,从 a 推导出的类型为 int(const 修饰符被省略),但由于我们在定义变量 b 时重新添加了 const 修饰符,因此变量 b 的类型将为 const int。
字符串字面量的类型推断
由于历史原因,C++中的字符串字面量具有特殊类型。因此,以下代码可能无法达到预期效果:
auto s { "Hello, world" }; // s will be type const char*, not std::string

若希望字符串字面量推导出的类型为 std::string 或 std::string_view,则需使用 s 或 sv 字面量后缀(详见第 5.7 课——std::string 介绍及第 5.8 课——std::string_view 介绍):
#include <string>
#include <string_view>
int main()
{
using namespace std::literals; // easiest way to access the s and sv suffixes
auto s1 { "goo"s }; // "goo"s is a std::string literal, so s1 will be deduced as a std::string
auto s2 { "moo"sv }; // "moo"sv is a std::string_view literal, so s2 will be deduced as a std::string_view
return 0;
}

但在这种情况下,最好不要使用类型推断。
类型推断与constexpr
由于 constexpr 不属于类型系统的一部分,因此无法在类型推导过程中被推导出来。然而,constexpr 变量隐含地具有 const 属性,该 const 属性将在类型推导过程中被移除(如有需要可重新添加):
int main()
{
constexpr double a { 3.4 }; // a has type const double (constexpr not part of type, const is implicit)
auto b { a }; // b has type double (const dropped)
const auto c { a }; // c has type const double (const dropped but reapplied)
constexpr auto d { a }; // d has type const double (const dropped but implicitly reapplied by constexpr)
return 0;
}

类型推导的优势与局限
类型推导不仅便捷,还具备诸多其他优势。
首先,若两个或多个变量在连续行中定义,其名称将自动对齐,从而提升代码可读性:
// harder to read
int a { 5 };
double b { 6.7 };
// easier to read
auto c { 5 };
auto d { 6.7 };
其次,类型推断仅适用于具有初始化器的变量,因此若养成使用类型推断的习惯,可帮助避免无意中出现未初始化的变量:
int x; // oops, we forgot to initialize x, but the compiler may not complain
auto y; // the compiler will error out because it can't deduce a type for y
第三,您可确保不会发生任何意外影响性能的转换:
std::string_view getString(); // some function that returns a std::string_view
std::string s1 { getString() }; // bad: expensive conversion from std::string_view to std::string (assuming you didn't want this)
auto s2 { getString() }; // good: no conversion required
类型推导也存在若干缺点。
首先,类型推导会使对象的类型信息在代码中变得模糊。尽管优秀的集成开发环境(IDE)能够显示推导出的类型(例如在变量上悬停时),但在使用类型推导时,基于类型的错误仍然更容易发生。
例如:
auto y { 5 }; // oops, we wanted a double here but we accidentally provided an int literal

在上面的代码中,如果我们显式指定 y 的类型为 double,那么即使我们不小心提供了 int 字面量初始化器,y 仍会被视为 double 类型。而通过类型推断,y 将被推断为 int 类型。
下面是另一个示例:
#include <iostream>
int main()
{
auto x { 3 };
auto y { 2 };
std::cout << x / y << '\n'; // oops, we wanted floating point division here
return 0;
}
在此示例中,我们难以明确判断这是整数除法还是浮点除法。
当变量为无符号类型时也会出现类似情况。由于我们不希望混用有符号与无符号值,因此明确知道变量具有无符号类型这一特性通常不应被模糊处理。
其次,如果初始化器的类型发生变化,使用类型推导的变量类型也会随之改变,这种变化可能出人意料。请看以下示例:
auto sum { add(5, 6) + gravity };
如果 add 的返回类型从 int 变更为 double,或者 gravity 从 int 变更为 double,那么 sum 的类型也将从 int 变更为 double。
总体而言,现代共识认为类型推断对对象而言通常是安全的,且这种做法能通过弱化类型信息来提升代码可读性,使代码逻辑更突出。
最佳实践
当对象类型无关紧要时,请为变量使用类型推断。
当需要与初始化器类型不同的特定类型时,或当对象在需要明确类型信息的上下文中使用时,请优先采用显式类型声明。
作者注
在后续课程中,当我们认为展示类型信息有助于理解某个概念或示例时,将继续使用显式类型而非类型推断。

浙公网安备 33010602011771号