二、C++11新特性:decltype类型推导

声明:该笔记是在学习《深入理解C++11》、《C++11/14高级编程 Boost程序库探秘》时做的总结,方便以后巩固复习!

二、decltype类型推导

2.1、 decltype

auto关键字能够在赋值语句里推导类型,但这只是C++语言里一种很少见的应用场景,要想在任意的场景都能够得到表达式的类型就需要使用关键字:decltype

decltype的形式和函数调用很像:

decltype(expression)  //获取表达式的类型--在编译期计算

decltype在技术和使用上和sizeof非常像,都需要编译器在编译期计算类型,但是sizeof返回的是整数,而decltype得到的是类型

decltype和auto的异同:

与auto不同点:

decltypr的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,decltype总是以一个普通的表达式为参数,返回该表达式的类型。

与auto相同点:

①、作为一个类型指示符,decltype可以将获得的类型来定义另外一个变量;

②、declttype类型推导也是在编译时进行的

#include <iostream>
#include <typeinfo>
using namespace std;
int main()
{
    
    
	int a;
	decltype(a) b = 0;
	cout<<"a的类型是"<<typeid(a).name()<<endl;
	cout<<"b的类型是"<<typeid(b).name()<<endl;
	
	float c;

	double d;
	decltype(c + d ) e;
	cout<<"e的类型是"<<typeid(e).name()<<endl;

	return 0;
}

输出:

a的类型是i  //i代表int
b的类型是i  
e的类型是d   //d代表double

可以看到变量b的类型由decltype(a)进行声明,表示b跟a这个表达式返回的类型相同;而e的类型则由(c + d)这个表达式返回的类型相同,c + d的表达式的类型为double,所以e的类型被decltype推导为double。

2.2、decltype的应用

①、decltype和typedf/using合用

在C++11 的头文件中常可以看到以下代码:

using size_t = decltype(sizeof(0));
using ptrdiff_t = decltype((int*)0- (int*)0);
using nullptr_t = decltype(nullptr);

size_t以及ptrdiff_t及nullptr都是由decltype推导出来的类型
这种定义方式的意义:
在一些常量、基本类型、运算符、操作符等基于被定义好的情况下,类型可以按照规则被推导出来。而使用using可以为这些类型取名;这样就颠覆了之前类型扩展需要将类型“映射”到基本类型的常规做法。

②、deltype在某些场景下使用增加代码的可读性

#include <vector>
using namespace std;
int main()
{
    
    
vector<int> vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin(); i < vec.end(); i++) 
{
    
    
	......
}
for (decltype(vec)::iterator i = vec.begin(); i < vec.end(); i++) 
{
    
    
	......
}  

定义了vector的iterator的类型。这个类型还可以在main函数中重用。
当遇到一些具有复杂类型的变量或表达式时,就可以利用decltype和typedef/using的组合来将其转化为一个简单的表达式,这样在以后的代码写作中可以提高可读性和可维护性。
此外可以看到decltype(vec)::iterator这样的灵活用法,这看起来跟auto非常类似,也类似于是一种“占位符”式的替代。

③、使用decltype重用匿名类型

enum class
{
    
    
	K1, K2, K3
}anon_e;    // 匿名的强类型枚举
union {
    
    
		decltype(anon_e) key;
		char* name;
}anon_u;     // 匿名的union联合体

struct {
    
    
		  int d;
           decltype(anon_u) id;
}anon_s[100];  // 匿名的struct数组

 int main() 
 {
    
    
 	decltype(anon_s) as;
    as[0].id.key = decltype(anon_e)::K1;    // 引用匿名强类型枚举中的值
    return 0;
  } 

这里我们使用了3种不同的匿名类型:匿名的强类型枚举anon_e、匿名的联合体anon_u,以及匿名的结构体数组anon_s。可以看到,只要通过匿名类型的变量名anon_e、anon_u,以及anon_s,decltype可以推导其类型并且进行重用。这些都是以前C++代码所做不到的。
不过匿名一般都有匿名理由,一般都不希望匿名后的类型被重用。

④、decltype可以适当扩大模板泛型的能力

#include <iostream>
#include <typeinfo>
using namespace std;

// s的类型被声明为decltype(t1 + t2)
template<typename T1, typename T2>
void Sum(T1 & t1, T2 & t2, decltype(t1 + t2) & s)
{
    
    
	s = t1 + t2;
    cout<<"s的类型是"<<typeid(s).name()<<endl;
}
int main() 
{
    
    
	int a = 3;
	long b = 5;
	float c = 1.0f, d = 2.3f;
	long e;
	float f;
	Sum(a, b, e);    // s的类型被推导为long
	Sum(c, d, f);    // s的类型被推导为float
}    

输出:

s的类型是l
s的类型是f

代码中的Sum函数模板增加了类型为decltype(t1+t2)的s作为参数,而函数本身不返回任何值。这样一来,Sum的适用范围增加,其返回的类型是根据t1 + t2推导而来的类型。不过这里还是有一定的限制,可以看到返回值的类型必须一开始就被指定,我们必须清楚Sum运算的结果使用什么样的类型来存储是合适的,这在一些泛型编程中依然不能满足要求。
解决的方法是结合decltype与auto关键字,使用追踪返回类型的函数定义来使得编译器对函数返回值进行推导。

2.3、decltype推导规则

①、表达式为普通变量或者普通表达式或者类表达式,在这种情况下,使用 decltype 推导出的类型和表达式的类型是一致的。

#include <iostream>
#include <string>
using namespace std;

class Test
{
    
    
public:
    string text;
    static const int value = 110;
};

int main()
{
    
    
    int x = 99;
    const int &y = x;
    decltype(x) a = x;
    decltype(y) b = x;
    decltype(Test::value) c = 0;

    Test t;
    decltype(t.text) d = "hello, world";

    return 0;
}

变量 a 被推导为 int 类型
变量 b 被推导为 const int & 类型
变量 c 被推导为 const int 类型
变量 d 被推导为 string 类型

②、表达式是函数调用,使用 decltype 推导出的类型和函数返回值一致。

class Test{
    
    ...};
//函数声明
int func_int();                 // 返回值为 int
int& func_int_r();              // 返回值为 int&
int&& func_int_rr();            // 返回值为 int&&

const int func_cint();          // 返回值为 const int
const int& func_cint_r();       // 返回值为 const int&
const int&& func_cint_rr();     // 返回值为 const int&&

const Test func_ctest();        // 返回值为 const Test

//decltype类型推导
int n = 100;
decltype(func_int()) a = 0;		
decltype(func_int_r()) b = n;	
decltype(func_int_rr()) c = 0;	
decltype(func_cint())  d = 0;	
decltype(func_cint_r())  e = n;	
decltype(func_cint_rr()) f = 0;	
decltype(func_ctest()) g = Test();	

变量 a 被推导为 int 类型
变量 b 被推导为 int& 类型
变量 c 被推导为 int&& 类型
变量 d 被推导为 int 类型
变量 e 被推导为 const int & 类型
变量 f 被推导为 const int && 类型
变量 g 被推导为 const Test 类型

函数 func_cint () 返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据),对于纯右值而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略掉这两个限定符,因此推导出的变量 d 的类型为 int 而不是 const int。

③、表达式是一个左值,或者被括号 ( ) 包围,使用 decltype 推导出的是表达式类型的引用(如果有 const、volatile 限定符不能忽略)。

#include <iostream>
#include <vector>
using namespace std;
class Test
{
    
    
public:
    int num;
};
int main() 
{
    
    
    const Test obj;    //带有括号的表达式    
    decltype(obj.num) a = 0;    
    decltype((obj.num)) b = a;    //加法表达式    
    int n = 0, m = 0;   
    decltype(n + m) c = 0;
    decltype(n = n + m) d = n;    
    return 0;
 }

obj.num 为类的成员访问表达式,符合场景 1,因此 a 的类型为 int
obj.num 带有括号,符合场景 3,因此 b 的类型为 const int&。
n+m 得到一个右值,符合场景 1,因此 c 的类型为 int
n=n+m 得到一个左值 n,符合场景 3,因此 d 的类型为 int&

2.4、cv限制符的继承与冗余的符号

①、与auto类型推导时不能“带走”cv限制符不同是:

decltype是能够“带走”表达式的cv限制符的。不过,如果对象的定义中有const或volatile限制符,使用decltype进行推导时,其成员不会继承const或volatile限制符。

#include <type_traits>
#include <iostream>
using namespace std;

const int ic = 0;
volatile int iv;
struct S 
{
    
     
	int i; 
};
const S a = {
    
    0};
volatile S b;
volatile S* p = &b;

int main() {
    
    
	cout << is_const<decltype(ic)>::value << endl;        // 1            
	cout << is_volatile<decltype(iv)>::value << endl;    // 1
	cout << is_const<decltype(a)>::value << endl;         // 1            
	cout << is_volatile<decltype(b)>::value << endl;     // 1
	cout << is_const<decltype(a.i)>::value << endl;      // 0, 成员不是const  
	cout << is_volatile<decltype(p->i)>::value << endl; // 0, 成员不volatile
}

输出:

1
1
1
1
0
0

这里使用了C++库提供的is_const和is_volatile来查看类型是否是常量或者易失的。可以看到,结构体变量a、b和结构体指针p的cv限制符并没有出现在其成员的decltype类型推导结果中。

与auto相同的是:

decltype从表达式推导出类型后,进行类型定义时,也会允许一些冗余的符号。 比如cv限制符以及引用符号&,通常情况下,如果推导出的类型已经有了这些属性,冗余的符号则会被忽略

#include <type_traits>
#include <iostream>
using namespace std;
int i = 1;
int & j = i;
int * p = &i;
const int k = 1;

int main() 
{
    
    
    decltype(i) & var1 = i;
    decltype(j) & var2 = i;      // 冗余的&, 被忽略
    cout << is_lvalue_reference<decltype(var1)>::value << endl;// 1, 是左值引用
    cout << is_rvalue_reference<decltype(var2)>::value << endl;// 0, 不是右值引用
    cout << is_lvalue_reference<decltype(var2)>::value << endl;// 1, 是左值引用
    //decltype(p)* var3 = &i;      // 无法通过编译              
    decltype(p)* var3 = &p;      // var3的类型是int**             
    auto* v3 = p;                  // v3的类型是int*
    v3 = &i;
    const decltype(k) var4 = 1; // 冗余的const,被忽略
} 

输出:

1
0
1

这里定义了类型为decltype(i) &的变量var1,以及类型为decltype(j) &的变量var2。
由于i的类型为int,所以这里的引用符号保证var1成为一个int&引用类型。而由于j本来就是一个int &的引用类型,所以decltype之后的&成为了冗余符号,会被编译器忽略,因此j的类型依然是int &。
特别要注意的是decltype§的情况。可以看到,在定义var3变量的时候,由于p的类型是int,因此var3被定义为了int**类型。这跟auto声明中,也可以是冗余的不同。在decltype后的号,并不会被编译器忽略。
var4中const可以被冗余的声明,但会被编译器忽略,同样的情况也会发生在volatile限制符上。

参考文章:https://subingwen.cn/cpp/autotype/#2-2-decltype%E7%9A%84%E5%BA%94%E7%94%A8

欢迎关注公众号:Kevin的嵌入式学习站
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44705488/article/details/121283384
今日推荐