auto 和 decltype 的改进

1. auto 的改进

autodecltype 基础 –> 自动类型推导

C++14 对 auto 关键字进行了非常显著的增强,主要解决了 C++11 中 auto 使用起来比较繁琐的问题,并引入了两大核心改进:函数返回类型推导泛型 lambda

1.1 函数返回类型推导

C++ 中的 auto 类型推导是 C++11 引入的核心特性之一。它主要遵循 模板类型推导 的规则,在 C++14 中允许函数使用 auto 推导返回类型(不含尾置返回类型)。

在 C++11 中必须使用尾置返回类型:

1
auto func() -> int { return 42; }

在 C++14 中可以直接使用 auto 进行返回值类型推导:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 编译器自动推导为 double
auto add(int x, double y)
{
return x + y;
}

// 甚至可以用于递归函数(但需要有一个非递归的返回语句)
auto factorial(int n)
{
if (n == 1) return 1; // 这里推导出 int
return n * factorial(n - 1);
}

// 使用 auto:类型剥除
auto func1()
{
int x = 5;
int& ref = x;
return ref; // 返回 int(引用被剥除)
}

在使用auto进行函数返回值类型推导的时候,需要注意以下几点:

  • 当变量不是指针或者引用类型时,推导的结果中不会保留constvolatile关键字

  • 如果函数有多个返回语句,它们推导出的类型必须一致。

  • 如果是递归调用,必须至少有一个非递归的返回语句,以便编译器确定返回类型。

1.2 lambda 参数支持 auto

C++14 允许 lambda 表达式参数使用 auto泛型 Lambda):

在 C++11 中必须指定参数类型:

1
auto lambda = [](int x, int y) { return x + y; };

在 C++14 中 lambda 表达式参数可以是 auto :

1
2
3
4
5
6
7
8
auto generic_lambda = [](auto x, auto y) { 
return x + y;
};

// 使用
generic_lambda(1, 2); // OK
generic_lambda(1.5, 2.3); // OK
generic_lambda("a", "b"); // 字符串拼接

2. decltype

在 C++14 中,关于 decltype 的改进和优化主要是为了弥补 C++11 中其在类型推导和易用性方面的不足。虽然 decltype 的核心语法规则没有改变,但 C++14 引入了新语法和上下文支持,使其更加强大和易用。

2.1 引入 decltype(auto)

这是 C++14 针对 decltype 最著名的扩展。它解决了 C++11 中 auto 推导规则(剥离引用和 const)与 decltype 推导规则(保留引用和 const)之间的割裂。

在 C++11 中,如果你想写一个返回函数调用结果的通用包装器,并保留原始的值类别(比如返回引用),你必须使用尾置返回类型:

1
2
3
4
5
6
// C++11 写法:冗长
template<typename T, typename U>
auto func(T& t, U& u) -> decltype(t + u)
{
return t + u;
}

C++14 允许我们直接写成这样:

1
2
3
4
5
6
// C++14 写法:简洁,且保留了 decltype 的推导规则
template<typename T, typename U>
decltype(auto) func(T& t, U& u)
{
return t + u;
}

推导函数返回值使用auto和使用decltype(auto)的特性对比:

  • auto:初始化表达式如果是引用,推导为值类型(拷贝)。
  • decltype(auto):初始化表达式如果是引用,推导为引用类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
std::string& getName();

// C++11 auto: 返回 string,丢失了引用语义
auto c11_name = getName();

// C++14 decltype(auto): 返回 string&,保留引用语义
decltype(auto) c14_name = getName();

// 函数示例
decltype(auto) getElement(std::vector<int>& vec, size_t index)
{
return vec[index]; // 漂亮地返回 int&
}

2.2 decltype 对括号的敏感性

括号敏感性虽然这是 decltype 自带的规则,但在 C++14 的 decltype(auto) 语境下显得尤为重要。C++14 明确了 decltype(auto) 对变量名和表达式的不同处理方式:

  • 如果 decltype 里的参数是一个不加括号的变量名(如 x),推导结果就是该变量的类型。

  • 如果decltype里的参数是一个表达式(如x+yvec[i]),推导结果取决于该表达式的值类别:

    只要 decltype 的参数被括号 ( ... ) 包裹,该参数在语法上就被视为一个表达式,而不是一个变量名。既然是表达式,接下来就要看这个表达式是左值 还是 右值

    • 如果是左值(L-value,如变量),推导结果为 类型的引用 (T&)。
    • 如果是右值(R-value,如临时对象或字面量),推导结果为 类型本身 (T)。

为了更清楚地解释,我们看 decltype(auto) 何时会因为括号而改变结果(注意这样可能会产生返回局部变量引用的隐患):

  • 情况1

    1
    2
    int x = 10;
    decltype(auto) a = x;

    不加括号,等价于 decltype(x),类型是 int

  • 情况2

    1
    2
    int x = 10;
    decltype(auto) b = (x);

    加括号,等价于 decltype((x))x 是左值表达式,它有内存地址,可以赋值。类型推导为 int&

  • 情况3

    1
    2
    3
    4
    5
    int x = 10;
    decltype(auto) test1()
    {
    return x;
    }

    等价于 decltype(x)x 是变量名,不是复杂的表达式。推导结果:int (值类型)

  • 情况4

    1
    2
    3
    4
    5
    int x = 10;
    decltype(auto) test2()
    {
    return (x);
    }

    等价于 decltype((x)),加了括号,被视为一个表达式。x 是左值,所以推导为左值引用。推导结果:int& (引用类型)。

    警告:如果 x 是局部变量,这里返回悬空引用!

    1
    2
    3
    4
    5
    6
    // 错误示范
    decltype(auto) test2()
    {
    int x = 10;
    return (x); // 警告⚠️:返回局部变量的应用
    }
  • 情况5

    1
    2
    3
    4
    5
    int x = 5, y = 9;
    decltype(auto) test3()
    {
    return (x+y);
    }

    等价于 decltype((x+y)),加了括号,被视为一个表达式。虽然 (x+y) 被视为一个表达式,但关键在于(x+y) 这个表达式的 <值类别> 是 <右值>,而不是 <左值>。

    因为x + y 的计算结果产生了一个临时数值(比如 10),这个临时数值没有在内存中具体的地址,不能被赋值,所以(x+y) 是一个右值表达式。