C++20:换了“心“的auto关键字

何爲换“心”

auto关键字是C++98就引入的关键字,C++98标准中,auto声明的变量为自动变量,就是说他具备自动生命周期。由于C++98规定函数代码段中声明的变量默认是auto变量,经常大家在声明变量时直接忽略auto。所以可以看出C++98中auto关键字是一个可有可无的关键字,无任何实质功能。但是从C++11开始,auto从原来可有可无转变成具备实质功能的C++关键字。这就是所谓的auto换“心”。

C++11标准委员会赋予了auto两个能力:第一,变量定义时可根据初始化表达式自动类型推导的能力;第二,声明函数返回值的占位符,允许函数后置返回类型。

C++14标准,C++17标准和C++17标准都分别对auto进行了升级扩展;C++14对auto函数占位符进行了简化,lambda形参声明允许使用auto;C++17将auto引入到非类型模板的声明和结构化绑定。C++20允许函数形参声明使用auto。

auto的标准演进

C++11

C++11标准委员会为auto引入了两个能力:第一,变量定义时可根据初始化表达式自动类型推导的能力;第二,声明函数返回值的占位符,允许函数后置返回类型。例如:

auto value = 1;   // 推导为int
auto name = "liuguang"; // 推导为const char *

auto min(int a, int b) -> int   // auto为int的占位符,这种返回类型后置是C语言早期的一种函数声明方式。
{
    
    
	return (a < b) ? (a): (b);
}

由于auto会在编译过程中自动完成类型推导,因此在编码过程中要确保代码类型可推导。例如:

auto i;  // 编译失败,编译期无法决策i的类型

此处之所以会编译错误,原因在于没有对i进行初始化,编译器无法确认i的具体类型。

C++14

C++14引入三个重要特性:第一,decltype(auto) ;第二,支持函数或 lambda 表达式的返回类型声明为auto,允许return语句的操作数推导返回auto类型。第三,允许lambda表达式使用auto形参声明。

auto x = 1 + 2;
decltype(auto) y = x;           // y的类型是int,持有x的副本          
decltype(auto) z = (x);         // z的类型int&, z是x的引用  

template<class T, class U>
auto add(T t, U u)              // 返回类型是 operator+(T, U) 的类型
{
    
     
	return t + u; 
}

auto lambda = [](int x) {
    
     return x + 3; };

auto lambda1 = [](auto a, auto b) {
    
    return a + b;}; // lambda1的返回类型为decltype(a+b)
auto value = lambda1(1, 3.0);   // value 被推导为double

C++17

从C++17标准开始,auto开始支持非类型模板的声明,auto非类型模板同样需要遵守非类型模板标准,即推导出的类型必须是可作为模板实参的。

template<auto n>   // C++17 auto 形参声明
auto f() -> std::pair<decltype(n), decltype(n)> // auto 不能从花括号初始化器列表推导
{
    
    
    return {
    
    n, n};
}

f<5>();      // n为int
f<'a'>();    // n为char
f<1.0>();    // 编译失败double不能作为模板参数

除非类型模板声明,C++17标准还允许auto 说明符应用于结构化绑定声明。例如:

auto [v, w] = f<100>(); // 结构化绑定声明

C++20

函数形参声明允许使用auto关键字 ,这种函数声明可简化函数模板。

void f1(auto); // 与 template<class T> void f(T) 相同

auto min(auto a, auto b)
{
    
    
	return (a < b) ? (a) : (b);
}

auto val1 = min(2, 5.0);  // val1推导为double
auto val2 = min(2.0, 5.0);// val2推导为double
auto val3 = min(2.0, 5);  // val3推导为double

auto推导规则

使用auto占位符声明变量必须初始化变量,由于初始化表达式存在多种形式,不同形式的表达式auto的推导规则也不尽相同,此部分主要介绍auto的推导规则。

按值初始化忽略cv限定符

在auto声明变量时,如果没有使用引用也没有使用指针。那么编译期推导时会忽略cv限定符。但是如果auto声明时使用了指针或引用,那么cv限定符不会忽略。例如:

const int i = 12;
auto j = i;   // j会被推导为int
auto &k = i;  // k声明时使用&,auto会被推导为const int,k类型为const int&
auto *x = &i; // x声明使用指针* auto会被推导为const int, x类型为const int*
const auto y = i; // auto 推导为int, y类型为const int

按引用初始化忽略引用属性

如果使用一个引用对象去初始化auto类型对象,那么原对象的引用属性会被忽略。例如:

const int i = 12;
auto &k = i;  // k声明时使用&,auto会被推导为const int,k类型为const int&
auto j = k;   // j 会推导为int 因为此处k的引用属性将会被忽略

auto与&&

在此需要特别说明的是auto与&&结合时,&&是万能引用而非右值引用。

如果初始化表达式为左值,根据&&引用折叠原理auto会决策为引用,如果初始化表达式为右值,auto会推导为表达式字面值类型。

int i = 100;
auto&& j = i;     // 引用折叠,auto&& 推导为int&, 所以j的类型为int&
auto&& k = 200;   // 引用折叠,auto&& 推导为int&&, 所以k的类型为int&&

auto同数组和函数

auto与数组结合,数组会退化为数组指针,auto与函数结合,函数也会退化为函数指针。

int array[100] = {
    
    0};
auto i = array;   // i推导为int*

auto min(int a, int b) -> int 
{
    
    
	return (a < b) ? (a): (b);
}

auto fn = min;   // fn 推导的类型为int (*)(int, int)

&&与三目运算符

&&与三目运算符结合进行类型推导,编译期总是使用数据范围更大的类型。例如:

auto min = (true) ? 100, 2.3;  // min会被推导为double,因为double比int类型范围更大。

auto与多变量

如果一个auto关键字声明多个变量,那么auto将会使用左结合性,也就是就近结合类型推导。此时只有满足声明类型统一才可以通过编译。否则会提示编译失败。

int value = 8;
auto *pValue = &value, n = 10;  // auto 推导为int, n也为int,可以编译通过

auto *pValue = &value, m = 10.0; // auto 推导为int, m也为int,m不能赋值10.0

auto与成员声明初始化

虽然C++11支持类成员变量声明初始化,但是auto却无法应用于类非静态成员初始化。仅可以应用于const static 成员变量。

struct Person
{
    
    
	auto age = 15;  // 编译失败,不允许这么声明
};

struct Person
{
    
    
	static const auto age = 15; // C++11可以编译通过 
};

在C++17标准,进一步放松限制,允许static auto成员变量声明初始化。

struct Person
{
    
    
	static auto age = 15; // C++17可以编译通过 
};

auto与列表初始化

C++11标准下,直接列表初始化和等号赋值列表初始化,均推导为std::initializer_list。

auto d = {
    
    1, 2};  // d的类型是 std::initializer_list<int>
auto n = {
    
    5};     // n 的类型是 std::initializer_list<int>
auto e{
    
    1, 2};     // C++11标准,e的类型是std::initializer_list<int>
auto m{
    
    5};        // C++11标准,m的类型是std::initializer_list<int>

C++17标准下,等号赋值列表初始化推导为std::initializer_list,直接赋值初始化仅允许单个元素。

auto d = {
    
    1, 2};  // d的类型是 std::initializer_list<int>
auto n = {
    
    5};     // n 的类型是 std::initializer_list<int>
auto e{
    
    1, 2};     // C++17标准,编译报错误
auto m{
    
    5};        // C++11标准,m的类型是int

两份代码一模一样,读者可对比C++11和C++17 auto列表初始化类型推导差异。

decltype(auto)

decltype(auto) 是C++14标准引入的占位约束。意义在于通过decltype推导表达式规则来推导auto。

auto a = 1 + 2;          // a 的类型是 int
decltype(auto) c1 = a;   // c1 的类型是 int,保有 a 的副本
decltype(auto) c2 = (a); // c2 的类型是 int&,它是 a 的别名

C++20中decltype(auto)增加的类型约束功能。关于类型约束笔者会在decltype相关的博客中详细论述。这里就不赘述,仅给出一个例子。

// 在它调用的函数返回引用的情况下
// 函数调用的完美转发必须用 decltype(auto)
template<class F, class... Args>
decltype(auto) perfectForward(F fun, Args&&... args) 
{
    
     
    return fun(std::forward<Args>(args)...); 
}

auto使用场景

在编码过程中,关于何时使用auto,不同的开发人员会有不同的见解和看法。但是这里面依然存在某些规律可寻。这里为大家介绍几种大家广泛认可的使用场景。

  • 明确知晓初始化类型时可使用auto,例如:
auto i = 2;
  • 复杂的类型,例如:lambda表达式,bind以及STL迭代器。lambda和bind类型有时候我们自己都很难描述,而STL迭代器则是因为类型太长而影响代码阅读。
std::unordered_multimap<std::string, int> unorderedMap;

unorderedMap.insert(std::make_pair("li", 10));
unorderedMap.insert(std::make_pair("liu", 50));
unorderedMap.insert(std::make_pair("wu", 49));

std::unordered_multimap<std::string, int>::iterator iter = unorderedMap.find("li"); 
//  auto iter = unorderedMap.find("li");  使用auto iter定义可简化为auto iter;
if (iter != unorderedMap.end()) //查找成功
{
    
    
    std::cout << "name: " << iter->first << "age: " << iter->second;
}

// 你可以准确的描述fnIsEven的具体类型吗? 在此只有auto最简单。
auto fnIsEven = [](int value) -> bool {
    
    
	return (0 == value % 2);
};

总结

本文详细为大家介绍了auto关键字的演进过程以及auto关键字的类型推导规则。希望对你有帮助。

猜你喜欢

转载自blog.csdn.net/liuguang841118/article/details/128195097