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;
}

image
image

首例中,由于 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;
}

image
image

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;
}

image
image

使用类型推导的变量也可使用其他限定符/修饰符,如 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;
}

image
image


类型推导必须有推导依据

对于既无初始化器又无空初始化器的对象,类型推导将失效。当初始化表达式类型为 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;
}

image

虽然基础数据类型的类型推断仅节省少量(甚至无)键盘输入,后续课程将展示类型复杂冗长的实例(某些情况下甚至难以推断)。此时使用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;
}

image
image

在上例中,变量 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

image

若希望字符串字面量推导出的类型为 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;
}

image

但在这种情况下,最好不要使用类型推断。


类型推断与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;
}

image


类型推导的优势与局限

类型推导不仅便捷,还具备诸多其他优势。

首先,若两个或多个变量在连续行中定义,其名称将自动对齐,从而提升代码可读性:

// 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

image

在上面的代码中,如果我们显式指定 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。

总体而言,现代共识认为类型推断对对象而言通常是安全的,且这种做法能通过弱化类型信息来提升代码可读性,使代码逻辑更突出。

最佳实践
当对象类型无关紧要时,请为变量使用类型推断。
当需要与初始化器类型不同的特定类型时,或当对象在需要明确类型信息的上下文中使用时,请优先采用显式类型声明。

作者注
在后续课程中,当我们认为展示类型信息有助于理解某个概念或示例时,将继续使用显式类型而非类型推断。

posted @ 2026-03-05 10:09  游翔  阅读(0)  评论(0)    收藏  举报