文章目录
一、引言
1、C++中类型推导的重要性
在C++中,类型推导是一个极其重要的概念,它允许编译器根据上下文自动确定变量的类型,从而简化了代码编写过程,提高了编程效率。类型推导不仅减少了程序员显式指定类型的次数,还降低了因类型不匹配而导致的错误风险。
在C++11及以后的版本中,auto
和decltype
成为了实现类型推导的两个关键工具。它们各自具有独特的功能和用途,为程序员提供了更多的选择。auto
关键字主要用于在变量声明时自动推导变量的类型,而decltype
则可以根据表达式的类型进行推导。两者在类型推导机制上有所不同,但在实际编程中往往可以相互补充,共同提高代码的可读性和可维护性。
二、auto关键字及其特性
编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。C++11中引入了auto
关键字,用它能够让编译器替我们去分析表达式所属的类型。和原来那些仅对应一种特定类型的说明符不同(如double
),auto
让编译器通过初始值来推算变量的类型。因此,auto定义的变量必须有初始值。
1、auto的基本定义与用途
auto
是C++11中引入的一个关键字,它用于在声明变量时让编译器自动推导出变量的类型。基本定义如下:
auto variableName = initializer;
在这里,initializer
是一个表达式,编译器会根据该表达式的类型来推导 variableName
的类型。auto
的主要用途在于简化代码,特别是在处理复杂类型或模板元编程时,可以避免显式写出冗长或难以理解的类型名。
2、auto在类型推导中的应用
auto
的类型推导是基于初始化表达式的,它可以推导基本类型、指针类型、引用类型、复杂类型(如结构体、类等)以及STL容器等。例如:
int x = 10;
auto a = x; // a 的类型是 int
std::vector<int> vec = {1, 2, 3};
auto b = vec; // b 的类型是 std::vector<int>
auto& c = vec; // c 是对 vec 的引用,类型为 std::vector<int>&
若想将引用类型设为auto
,此时原来的初始化规则仍然适用:
int i = 0;
auto &a = i; //正确: a是一个整型常量引用,绑定到 i
auto &b = 42; //错误: 不能为非常量引用绑定到字面值,因此引用会忽略顶层const
const auto &b = 42; //正确: 可以为常量引用绑定字面值
设置一个auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果给出示护照绑定一个引用,则此时的常量就不是顶层常量了。
在模板编程中,auto
尤其有用,因为它可以自动推导模板参数的类型:
template<typename T>
void foo(T t) {
auto bar = t; // bar 的类型与 t 相同
}
3、auto的局限性及需要注意的问题
尽管 auto
提供了很大的便利,但它也有一些局限性和使用上需要注意的问题:
- 不能推导数组类型:使用
auto
推导数组时会退化为指针。
int arr[10];
auto ptr = arr; // ptr 的类型是 int* 而不是 int[10]
- 忽略顶层const,但底层const会保留:如果初始化表达式带有顶层
const
修饰符,auto
推导出来的类型不会包含这个const
。
int i = 0;
const int ci = i, & cr = ci;
auto b = ci; // b是int ci的顶层const属性被忽略了
auto c = cr; // c是int cr是ci的别名,ci本身是一个顶层const
auto d = &i; // d是int*
auto e = &ci; // e是 const int* 对常量对象取地址是一种底层const
*e = 66; //报错:表达式必须是可修改的左值
- 不能推导出引用类型:使用
auto
推到引用类型,编译器会以引用对象的类型作为auto的类型。
int i = 0,&r = i;
auto a = r; //此时a是int类型
- 初始值必须都是同种类型:要在一个语句中定义多个变量,切记 & 和 * 只从属某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型。
int i = 0;
const int ci = i;
auto k =ci , &l = i ; //正确: k是int,l是int&
auto &m =ci, *p = &ci; //正确: m 是对const int&,p是const int *。
auto &n = i, *p2 = &ci; //错误: i是int 而 &ci是const int
//报错: 对于此实体“auto”类型是 "const int",但之前默示为 "int"
- 初始化器必须存在:使用
auto
声明变量时必须提供初始化器,否则编译器无法推导类型。
在使用auto构建数组时,如果提供了一个括号包围的初始化器,就可以使用auto
从此初始化器来推荐我们想要分配的对象的类型。但,由于编译器要用初始化器的类型来推断要分配的类型,只有当哭号中仅有单一初始化器时才可以使用auto
。
auto p1 = new auto(obj); //p指向一个与obj类型相同的对象
auto p2 = new auto{a,b,c}; //括号中只能有一个初始化器
补充:
顶层const可以表示任意的对象是常量,底层const则于指针和引用等复合类型的基本类型部分有关,比较特殊的是指针,指针既可以是底层
const
,也可以是顶层const
。即,对于指针来说:顶层const是表示指针本身是一个常量。底层const是表示指针指向的对象是一个常量。
三、decltype关键字及其特性
decltype
是C++11新增的一个关键字,用于推导表达式的类型。其基本定义可以理解为“declared type”,即“声明的类型”。
1、decltype的基本定义与用途
decltype
的主要功能是允许编译器在编译时期根据给定的表达式来推断其类型,而不仅仅是基于变量的初始化表达式。这使得decltype
在泛型编程和模板元编程中特别有用,可以解决由于类型由模板参数决定而难以或无法直接表示的问题。
与auto
关键字不同,auto
主要基于变量的初始化表达式来推导类型,而decltype
则允许基于任何给定的表达式来查询类型。但在此过程中,不实际计算表达式的值。例如,它可以用于推导函数的返回类型,或者用于推导与已有变量类型相同的新变量的类型。
2、decltype在类型推导中的应用
decltype
在类型推导中的表现非常灵活和强大。它可以根据表达式的实际类型进行推导,而不会实际计算表达式的值。这使得它能够在不执行表达式的情况下获取其类型信息。
例如,假设有一个函数foo()
返回一个int
类型的值,我们可以使用decltype
来推导并声明一个与foo()
返回类型相同的变量:
int foo();
decltype(foo()) result; // result的类型是int
同样地,如果我们有一个变量x
,我们可以使用decltype
来声明一个与x
类型相同的新变量y
:
int x = 42;
decltype(x) y; // y的类型是int
3、decltype的局限性及需要注意的问题
decltype
处理顶层const
和引用的方式与auto
有些许不同。
- 理解引用和顶层
const
:如果decltype
使用的表达式是一个变量,则decltype
返回该变量的类型(包括顶层const
和引用在内)
const int ci = 0, & cj = ci;
decltype(ci)x = 0; //x的类型是 const int
decltype(cj)y = x; //y的类型是 const int&,y绑定到变量x
decltype(cj)z; //错误:z是一个引用,必须初始化
因为cj
是一个引用,decltype(cj)
的结果就是引用类型,因此作为引用的 z 必须被初始化。
- 表达式的值类别:
decltype
根据表达式的值类别(左值或右值)推导类型。对于左值表达式,decltype
会推导出左值引用类型;对于右值表达式,会推导出非引用类型。
如果 decltype
使用的表达式不是一个变量,则decltype
返回表达式结果对应的类型。有些表达式将向decltype
返回一个引用类型。一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:
int i = 42, * p = &i, & r = i;
decltype(r + 0)b; //正确: 加法的结果是int,因此b是一个(未初始化的)int
decltype(*p)c; //错误: c是int&,必须初始化
因为r是一个引用,因此decltype(r)
的结果是引用类型。如果想让结果类型是 r 所指的类型,可以把 r 作为表达式的一部分,如r+0
,显然这个表达式的结果将是一个具体值而非一个引用。
另一方面,如果表达式的内容是解引用操作,则decltype
将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)
的结果类型就是int*
,而非int
。
- 括号的影响:
decltype
对括号的使用非常敏感。加上括号的变量会被视为表达式,导致推导出的类型可能是引用类型。因此,在使用decltype
时,需要注意是否使用了括号,以避免类型推导错误。
decltype
和auto
的另一处重要区别是,decltype
的结果类型与表达式形式密切相关。有一种情况需要特别注意:对于decltype
所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。**如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。**变量是一种可以作为赋值语句左值的特殊表达式,所以这样的 decltype
就会得到引用类型:
//decltype的表达式如果是加上了括号的变量,结果将是引用
int i =2;
decltype((i))d; //错误:d是int&,必须初始化
decltype(i)e; //正确:e是一个(未初始化的)int
- 与
auto
的差异:decltype
和auto
在类型推导上存在显著差异。auto
总是推导出一个对象的类型,而decltype
则根据表达式的形式推导类型。因此,在使用decltype
时,需要明确区分它与auto
的不同之处,以避免混淆和错误。
⚠️ decltype((variable))
的结果永远是引用,而decltype(variable)
结果只有当variable
本身就是一个引用时才是引用。
四、auto与decltype的区别
auto与decltype在C++中都是用于类型推导的关键字,但它们之间存在显著的区别。
1、类型推导机制的不同
- auto的类型推导规则:
- 当初始化语句中存在单一类型时,
auto
会将变量的类型推导为该类型。例如,auto x = 10;
将x推导为int类型。 - 当初始化语句中存在多个表达式且类型不完全相同时,
auto
会推导出“最宽泛”的类型。这通常意味着如果有一个类型是其他类型的隐式转换目标类型,auto
将推导为该目标类型。 - 如果初始化语句中存在
const
或volatile
限定符,auto
会保留这些限定符。
- 当初始化语句中存在单一类型时,
请注意,auto
在推导类型时有时会忽略顶层const
(定义变量本身是个常量),但会保留底层const
(定义变量指向的对象是个常量)。
- decltype的类型推导规则:
- 如果表达式是一个标识符(变量名或函数名),则推导出它的类型。
- 如果表达式是一个函数调用,返回值的类型就是推导结果的类型。
- 如果表达式是一个左值,则推导出它的类型为引用类型。
- 如果表达式是一个右值,则推导出它的类型为非引用类型。
decltype
会保留顶层const
,这与auto
不同。
此外,decltype
的结果类型与表达式的形式密切相关。如果变量名上加了一对括号,得到的类型与不加括号时会有不同。具体地说,双括号必为引用类型,单括号要表达式本身是引用类型才为引用类型,而auto
没有这样的变化。
2、使用场景的差异
-
auto更适用于简单的类型推导:
- 在处理迭代器和范围循环时,
auto
非常有用,可以自动推导出迭代器和元素的类型。 - 当函数的返回类型比较复杂或依赖于函数参数时,可以使用
auto
来简化返回类型的指定。 - 在模板编程中,
auto
也可以用于简化模板函数或模板类的定义。
- 在处理迭代器和范围循环时,
-
decltype更适用于复杂类型推导及与模板的结合:
- 当需要推导复杂表达式的类型时,特别是涉及到引用和
const
时,decltype
非常有用。 - decltype可以避免不必要的类型转换,直接使用表达式的真实类型。
- 在模板元编程中,decltype经常用于推导依赖于模板参数的复杂类型。
- 当需要推导复杂表达式的类型时,特别是涉及到引用和
五、auto与decltype的联系
auto类型推导可以自动地根据初始化的表达式推断出变量的类型,使得程序员在编写代码时不必显式指定变量类型。这使得代码更加简洁,减少了冗余,并提高了可读性。然而,auto类型推导在某些情况下可能不够精确,特别是当涉及到引用、指针、常量修饰符等复杂类型时。
相比之下,decltype类型推导则提供了更高的精度和灵活性。它根据表达式的类型和值类别推导出类型,并且可以精确地处理引用、常量性和其他类型属性。decltype尤其适用于那些需要根据表达式类型而非赋值来推断变量类型的场景。
下面是一个示例,展示了如何在同一代码片段中结合使用auto和decltype:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用auto进行简单的类型推导
auto it = vec.begin();
auto val = *it;
// 使用decltype进行精确的类型推导
decltype(vec.begin()) preciseIt = vec.begin();
decltype(auto) preciseVal = *it;
// C++14: 使用decltype(auto)保留引用和或cv(const和volatile)限定符。
// 输出推导出的类型
std::cout << typeid(it).name() << std::endl; // 输出auto推导出的类型
std::cout << typeid(preciseIt).name() << std::endl; // 输出decltype推导出的类型
std::cout << typeid(val).name() << std::endl; // 输出auto推导出的类型
std::cout << typeid(preciseVal).name() << std::endl; // 输出decltype(auto)推导出的类型
// 输出值以验证类型推导的正确性
std::cout << "Value from auto: " << val << std::endl;
std::cout << "Value from decltype(auto): " << preciseVal << std::endl;
return 0;
}
在这个示例中,我们首先使用auto
推导了迭代器it
和迭代器所指向的值val
的类型。然后,我们使用decltype
推导了与it
相同类型的preciseIt
。
decltype(auto)
是C++14引入的一个特性,它结合了decltype
的精确性和auto
的便捷性。使用decltype(auto)
时,编译器会根据表达式的类型推导出变量的类型,并保留该表达式的任何引用或cv(const和volatile)限定符。
最后,我们使用typeid
运算符输出了推导出的类型,并通过输出值验证了类型推导的正确性。
这个示例展示了auto和decltype在类型推导上的互补性。auto提供了简洁的类型推导,而decltype则提供了精确的类型推导。通过结合使用它们,我们可以在需要的地方获得所需的类型推导精度和灵活性。