C++ 的 auto 与 decltype
2025-04-01

auto 会去除 &、const 和 volatile 修饰符

1
2
3
4
5
6
7
8
9
10
int a = 40;
int& ref = a;

// b 是 int 类型,不是 int& 类型
auto b = ref;
b = 20;

// 输出 40
// 因为 b 是 int 类型,它是 a 的一个副本
std::cout << "a: " << a << std::endl;

decltype 实用场景

decltype(auto) 完美类型推断

decltype(auto) 能顺利解决上面的问题,它会保留变量的所有属性。

1
2
3
4
5
6
7
8
9
int a = 40;
int& ref = a;

// b 是 int& 类型
decltype(auto) b = ref;
b = 20;

// 输出 20,成功修改了 a 的值
std::cout << "a: " << a << std::endl;

在泛型代码中进行返回值类型转发

另一个场景是,在泛型代码中,我们希望能够完美转发一个返回值类型,而事先并不知道处理的是一个引用还是一个值。decltype(auto) 赋予了你这种能力:

1
2
3
4
5
6
7
8
9
template<class Fun, class... Args>
decltype(auto) Example(Fun fun, Args&&... args)
{
// fun(...) 的返回类型是什么,Example 的返回类型就是什么
// 如果 fun 返回 int&, Example 就返回 int&
// 如果 fun 返回 int&&,Example 就返回 int&&
// 如果 fun 返回 int, Example 就返回 int
return fun(std::forward<Args>(args)...);
}

decltype(auto) 会像 decltype 那样推导 fun(...) 表达式的类型,并保留其值类别(左值/右值)和 const/volatile 限定符,从而实现返回类型的完美转发。

在递归模板中延迟返回值类型推导

当模板的返回类型被指定为 decltype(iter(Int<i-1>{})) 而不是 decltype(auto) 时,遇到了模板实例化过程中的无限递归问题。

原始(有问题)代码可能类似:

1
2
template<int i>
constexpr auto iter(Int<i>) -> decltype(iter(Int<i-1>{})); // 导致无限递归

使用 decltype(auto) 的修正后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<int i>
struct Int {};

constexpr auto iter(Int<0>) -> Int<0>; // 递归终点

template<int i>
constexpr auto iter(Int<i>) -> decltype(auto) // 使用 decltype(auto)
{
// 返回类型将在函数体内的 return 语句被处理时才确定
return iter(Int<i-1>{});
}

int main() {
// iter(Int<10>{}) 的类型推导会逐层递归,直到 iter(Int<0>{})
// 最终确定返回类型为 Int<0>
decltype(iter(Int<10>{})) a;
// 此时 a 的类型是 Int<0>
}

这里使用 decltype(auto) 是为了延迟返回类型的推导,直到模板实例化过程递归结束后,根据函数体内的 return 语句来确定实际的返回类型,从而避免了在确定函数签名时就进行递归推导导致的无限循环。

参考资料

  1. What are some uses of decltype(auto)?