🌎 中文 | English

尾置返回类型 - trailing return type

尾置返回类型是 C++11 引入的一种新的函数声明语法 auto func(...) -> ReturnType, 把返回类型从函数名前面挪到参数列表之后. 它解决了 "返回类型依赖参数" 在传统语法下根本写不出来的问题, 同时也是 lambda 显式指定返回类型的统一语法形式

为什么引入?

  • 传统语法下, 返回类型写在函数名前面, 此时参数还没有出现在作用域中, 无法用参数去推导返回类型
  • 模板编程中经常需要 "返回类型 = 某个表达式的类型", 没有这种语法, decltype 推导依赖参数的返回类型几乎不可写
  • 让函数签名的形态更统一: lambda、普通函数、模板函数都能用同一种 auto ... -> T 形式

和传统返回类型语法有什么区别?

  • 传统写法: ReturnType func(Args...), 返回类型在最前面
  • 尾置写法: auto func(Args...) -> ReturnType, 用 auto 占位, 真正的返回类型放在 -> 之后
  • 两者大多数情况下完全等价, 但只有尾置写法能在 -> 之后引用参数名, 这是它真正不可替代的能力

一、基础用法和场景

基本语法 - auto + ->

把返回类型从函数名前移到参数列表之后, 中间用 -> 连接. 函数名前用 auto 占位, 表示 "返回类型在后面写"

// 传统写法
int add(double a, int b) {
    return a + b;
}

// 尾置返回类型写法 - 等价
auto add(double a, int b) -> int {
    return a + b;
}

这两种写法在编译结果上完全一致. 单看普通函数, 尾置写法只是风格差异, 并没有带来新的能力

配合 decltype 推导依赖参数的返回类型

尾置返回类型真正的杀手锏在模板里. 写一个泛型 add, 想让它支持 int、double、Point 等任意可相加的类型, 返回值是 a + b 的类型, 但具体是什么取决于 T1 和 T2

传统写法写不出来, 因为返回类型出现时, a 和 b 还没在作用域里

// 写不出来 - 此处 a, b 还未声明
decltype(a + b) add(T1 a, T2 b);

尾置语法把返回类型移到参数之后, 此时 a、b 已经在作用域中, 就可以用 decltype 直接推导

template<typename T1, typename T2>
auto add(const T1 &a, const T2 &b) -> decltype(a + b) {
    return a + b;
}

add(1, 2);     // 返回 int
add(1.1, 2);   // 返回 double
add(1, 2.1);   // 返回 double

这是尾置返回类型唯一不可被替代的核心场景: 让返回类型表达式可以引用参数名

在 C++14 之后, 普通函数可以直接写 auto add(...) 让编译器从 return 语句推导, 大多数情况不再需要写 -> decltype(a + b). 但 C++11 仍然必须用尾置语法

lambda 显式指定返回类型

lambda 没有名字, 也就没有 "返回类型写哪里" 的选择 - 它从一开始就只能用尾置语法

auto add = [](double a, double b) -> int {
    return a + b;  // 显式截断为 int
};

add(1.1, 2.1); // 3, 不是 3.2

不写 -> int 时, lambda 会自己推导返回类型为 double; 显式标注后, 返回值会被转换成指定的类型. 这是 lambda 控制返回类型的标准做法

嵌套类型 / 成员类型作为返回值

当返回类型是某个类的内嵌类型时, 传统写法需要在前面写完整限定 typename Class::Inner, 模板里还要加 typename 关键字, 又长又啰嗦

template<typename T>
typename std::vector<T>::iterator find_first(std::vector<T> &v, T x);

尾置写法可以让函数名先出现, 在 -> 之后写返回类型. 在某些类成员函数定义中, 可以省去重复的类名前缀

struct Box {
    struct Inner { /* ... */ };

    auto make() -> Inner;  // 返回类型只写 Inner
};

auto Box::make() -> Inner {  // 此时已经在 Box 的作用域内
    return Inner{};
}

二、注意事项

auto 在尾置语法中只是占位符

尾置写法里的 auto 不是类型推导, 它只是一个语法占位符, 真正的类型由 -> 之后给出. 这点和 C++14 的 auto func() { return ...; } 不同, 后者才是真正让编译器去推导

auto add(double a, int b) -> int {  // C++11: 返回类型由 -> int 显式给出
    return a + b;
}

auto add(double a, int b) {         // C++14: 返回类型由编译器从 return 语句推导
    return a + b;
}

C++11 中如果只写 auto func() 而省略 ->, 是编译错误

何时用尾置, 何时用传统

不要把所有函数都改成尾置写法. 经验法则:

  • 返回类型依赖参数 (decltype(a + b) 之类) -> 必须用尾置
  • lambda 想显式指定返回类型 -> 必须用尾置
  • 类成员函数返回内嵌类型, 想省掉 Class:: 前缀 -> 尾置更简洁
  • 普通函数, 返回类型简单 (int / void / std::string) -> 传统写法可读性更好, 没必要换

不是所有 "auto func" 写法都是尾置返回类型

auto func() -> int (C++11 尾置) 和 auto func() { return 1; } (C++14 返回类型推导) 写起来都以 auto 开头, 但语义完全不同

  • 前者: auto 是占位, 真实类型由 -> 给出, 编译器不需要看函数体
  • 后者: 编译器必须看 return 语句才能推导出返回类型, 函数声明和定义就不能完全分离 (头文件里只放声明就拿不到返回类型)

在写需要在头文件中声明、源文件中实现的函数时, 这个区别会直接影响是否能正常编译

返回类型仍然受常规规则约束

尾置返回类型只是位置变了, 类型本身的规则没变:

  • 不能返回函数 / 数组 (要返回函数指针 / 数组指针)
  • decltype 推导出来的引用 / const 限定都会保留
  • 派生类重写虚函数时, 返回类型仍要满足协变规则 (covariant return)
int arr[3];
// auto func() -> int[3];  // 错误: 不能返回数组
auto func() -> int(*)[3];   // ok: 返回数组指针

三、练习代码

练习代码主题

练习代码自动检测命令

d2x checker trailing-return-type

四、其他