10-9 函数的类型推断

考虑以下程序:

int add(int x, int y)
{
    return x + y;
}

当编译此函数时,编译器将求值 x + y 的结果为 int 类型,并确保返回值类型与函数声明的返回类型匹配(或返回值类型可转换为声明的返回类型)。


使用auto进行返回类型推导

由于编译器本就需要从 return 语句推导返回类型(以确保值可转换为函数声明的返回类型),C++14 扩展了 auto 关键字以实现函数返回类型推导。具体实现是将 auto 关键字替换函数声明中的返回类型。

例如:

auto add(int x, int y)
{
    return x + y;
}

由于返回语句返回的是 int 值,编译器将推断该函数的返回类型为 int。

使用 auto 返回类型时,函数内所有返回语句必须返回相同类型的值,否则将导致错误。例如:

auto someFcn(bool b)
{
    if (b)
        return 5; // return type int
    else
        return 6.7; // return type double
}

image

在上例函数中,两个返回语句返回不同类型的值,因此编译器将报错。

若因特殊需求需要此类情况,可通过两种方式解决:明确指定函数返回类型(此时编译器会尝试将所有不匹配的返回表达式隐式转换为显式类型),或显式将所有返回语句转换为相同类型。在上例中,后者可通过将5改为5.0实现,但对于非字面量类型也可使用static_cast。


返回类型推导的优势

返回类型推导的最大优势在于:由编译器推导函数返回类型可消除类型不匹配风险(避免意外转换)。

当函数返回类型易变(即实现变更可能导致返回类型改变)时,此特性尤为重要。此时显式声明返回类型意味着:实现发生影响性变更时需更新所有相关返回类型。若运气好,编译器会在相关返回类型未更新时报错;若运气差,则会出现非预期的隐式转换。

在其他情况下,函数的返回类型可能冗长复杂,或不够直观。此时可使用auto简化:

// let compiler determine the return type of unsigned short + char
auto add(unsigned short x, char y)
{
    return x + y;
}

image

我们在第11.8节——带有多个模板类型的函数模板中,将进一步探讨此类情况(以及如何表达此类函数的实际返回类型)。


返回类型推导的弊端

返回类型推导存在两大主要缺陷:

  1. 使用 auto 返回类型的函数必须在使用前完成完整定义(仅前向声明不足以生效)。例如:
#include <iostream>

auto foo();

int main()
{
    std::cout << foo() << '\n'; // the compiler has only seen a forward declaration at this point

    return 0;
}

auto foo()
{
    return 5;
}

在作者的机器上,这会导致以下编译错误:

image

这很合理:前向声明提供的信息不足以让编译器推断函数的返回类型。这意味着返回 auto 类型的普通函数通常只能在其定义文件内部调用。

  1. 当对象使用类型推断时,初始化器始终作为同一语句的一部分存在,因此通常不难确定将推断出的类型。但函数类型推断则不同——函数原型无法揭示其实际返回类型。优秀的编程IDE应明确显示函数的推断类型,若缺乏此功能,用户就必须深入函数体内部才能确定返回类型,这大大增加了出错概率。通常我们倾向于明确声明接口中的类型(函数声明即接口)。

与对象类型推导不同,函数返回类型推导的最佳实践尚未形成共识。我们建议普遍避免使用函数返回类型推导。

最佳实践
优先采用显式返回类型而非类型推导(除非返回类型无关紧要、难以表达或易受影响)。


尾随返回类型语法

auto 关键字也可用于采用尾随返回语法trailing return syntax声明函数,即在函数原型的其余部分之后指定返回类型。

请看以下函数示例:

int add(int x, int y)
{
  return (x + y);
}

使用尾部返回语法,这段代码可等效写为:

auto add(int x, int y) -> int
{
  return (x + y);
}

在此情况下,auto 不会执行类型推导——它只是使用尾随返回类型的语法组成部分。

为何要使用这种写法?原因如下:

  1. 对于具有复杂返回类型的函数,尾随返回类型能提升函数的可读性:
#include <type_traits> // for std::common_type

std::common_type_t<int, double> compare(int, double);         // harder to read (where is the name of the function in this mess?)
auto compare(int, double) -> std::common_type_t<int, double>; // easier to read (we don't have to read the return type unless we care)
  1. 尾随返回类型语法可用于对齐函数名称,使连续的函数声明更易于阅读:
auto add(int x, int y) -> int;
auto divide(double x, double y) -> double;
auto printSomething() -> void;
auto generateSubstring(const std::string &s, int start, int len) -> std::string;

对于进阶读者
3. 如果我们有一个函数,其返回类型必须根据函数形参的类型进行推断,那么普通的返回类型将无法满足需求,因为编译器在该位置尚未看到形参。

#include <type_traits>
// note: decltype(x) evaluates to the type of x

std::common_type_t<decltype(x), decltype(y)> add(int x, double y);         // Compile error: compiler hasn't seen definitions of x and y yet
auto add(int x, double y) -> std::common_type_t<decltype(x), decltype(y)>; // ok
  1. 尾随返回语法在C++某些高级特性中也是必需的,例如lambda表达式(详见第20.6课——lambda表达式(匿名函数)入门)。

目前我们建议继续使用传统函数返回语法,除非遇到必须使用尾随返回语法的情况。


函数形参类型无法使用类型推断

许多刚接触类型推导的新手程序员会尝试类似这样的代码:

#include <iostream>

void addAndPrint(auto x, auto y)
{
    std::cout << x + y << '\n';
}

int main()
{
    addAndPrint(2, 3); // case 1: call addAndPrint with int parameters
    addAndPrint(4.5, 6.7); // case 2: call addAndPrint with double parameters

    return 0;
}

image

遗憾的是,类型推导不适用于函数参数。在C++20之前,上述程序无法编译(会报错提示函数参数不能使用auto类型)。

在C++20中,auto关键字得到了扩展,使得上述程序能够编译并正常运行——但此时auto并未触发类型推导,而是激活了名为函数模板的特性,该特性正是为处理此类情况而设计。

相关内容
我们在第11.6节——函数模板中介绍了函数模板,并在第11.8节——带有多个模板类型的函数模板中讨论了函数模板中auto的使用。

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