C++11之decltype类型推导(使用场景、推导四规则、cv限定符)

简介: C++11之decltype类型推导(使用场景、推导四规则、cv限定符)

typeid与decltype

在学习decltype之前,我们先了解一下typeid运算符。typeid 运算符用来获取一个表达式的类型信息。需要包含<typeinfo>头文件才可以使用。

主要使用分为俩种场景:


1.对于基本类型(int、float 等C++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。

2.对于类类型的数据(对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。


我们可以通过name方法获取到这个类型,通过hash_code方法返回该类型唯一的哈希值(值得注意的是hash_code是在运行时获取的信息,hash_code也是C++11新加入的)。下面就通过hash_code方法判断了俩个变量的类型是否一致。

#include <iostream>
#include <typeinfo>
using namespace std;
class A{};
class B{};
int main()
{
  A a;
  B b;
  cout << typeid(a).name() << endl;
  cout << typeid(b).name() << endl;
  A c;
  bool ret = (typeid(a).hash_code() == typeid(b).hash_code());
  bool ret2 = (typeid(a).hash_code() == typeid(c).hash_code());
  cout << "Same type?" << endl;
  cout << "A and B?" << boolalpha << ret << endl;
  cout << "A and C?" << boolalpha << ret2 << endl;
  return 0;
}

运行结果:

class A
class B
Same type?
A and B?false
A and C?true


类型推导是用于模板编程和泛式编程中的。因为在其他编程中,各类型的确定的,不需要类型推导。而在泛式编程中,类型就是未知的,例如下面这个例子,在编译期T的类型是确定不了的。所以才引入了类型推导。最终定为了autodecltype,但是俩者功能并不相同。

template<typename  T>
void test(T t)
{
  ;
}


decltype的使用是非常简单的。语法:decltype(type) 变量名;

#include <typeinfo>
#include <iostream>
using namespace std;
int main()
{
  int i = 1;
  decltype(i) j = 0;
  cout << typeid(j).name() << endl;
  float a;
  double d;
  decltype(a + d) c;
  cout << typeid(c).name() << endl;
  return 0;
}

运行结果:

int
double

deletype以一个表达式或者变量为参数,然后返回该表达式/变量的类型,是一个类型指示符。decltype类型推导和auto自动类型一样都是编译期确定。

decltype的使用场景

增加代码的可读性、简洁性

在使用迭代器时,每次都需要很长的迭代器类型,例如map<int,int>::iterator ,使用decltype就可以将类型进行重定义,简化代码,提高可读性。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
  vector<int> arr(10);
  typedef decltype(arr.begin()) vecIter;
  for(vecIter i = arr.begin(); i < arr.end(); ++i)
  {
    *i = 1;
  }
  for (decltype(arr)::iterator i = arr.begin(); i < arr.end(); ++i)
  {
    cout << *i << " ";
  }
  cout << endl;
  return 0;
}


运行结果:

1 1 1 1 1 1 1 1 1 1


获取匿名自定义类型的类型

在一般情况下,我们定义的匿名枚举类型、匿名联合体/共用体、匿名结构体,是无法二次创建变量的。但是有了decltype就可以实现上述的功能。


下面通过decltype类型指示符通过匿名结构体数组变量推导出对应的匿名枚举类型。

#include <iostream>
enum  // 匿名枚举
{
  A,
  B,
  C,
}test;
union // 匿名联合体/共用体
{
  decltype(test) key;
  char* name;
}test2;
struct // 匿名结构体数组
{
  int d;
  decltype(test2) id;
}test3[10];
int main()
{
  decltype(test3) stu;
  stu[0].id.key = decltype(test)::A; // 引用匿名强类型枚举的值
  return 0;
}

用于泛式编程

见下面这个例子,Sum函数模板增加了一个类型为decltype(t1 + t2)的参数作为出参。这个出参的类型是根据T1T2入参类型共同决定的。如果入参的类型是数组的话就需要为数组提供特殊的重载版本。

template<typename T1, typename T2>
void Sum(T1& t1, T2& t2, decltype(t1 + t2)& s)
{
  s = t1 + t2;
}
void Sum(int a[], int b[], int c[])
{
  // 数组版本
}
int main()
{
  int a = 34;
  long int b = 5;
  float c = 1.0f;
  float d = 2.3f;
  long int e = 0;
  float f = 0;
  Sum(a, b, e);
  Sum(c, d, f);
  int arr1[5];
  int arr2[5];
  int arr3[5];
  Sum(arr1, arr2, arr3);
  return 0;
}


编译Sum(a,b,e)时,因为入参的类型是intlong int 所以,运算后出参类型就是long int。其他同理。

推导函数的返回值类型

基于decltype的模板类result_of,可以推导函数的返回值。

#include <type_traits>
using namespace std;
typedef double (*func)();
int main()
{
  result_of<func()>::type f = 1; // 推导函数的返回值
  std::cout << typeid(f).name() << std::endl;
  return 0;
}

运行结果:

double


decltype推导四规则

编译器在推导时会依照以下四个规则: decltype(e)

1.如果e是一个没有带括号的**标记符表达式(id-expression)**或者类成员访问表达式,那么decltype(e)就是e所命名实体的类型。此外,如果e是一个被重载的函数,则会导致编译时错误。

2.否则,假设e的类型是T。如果e是一个将亡值(xvalue),那么decltype(e)为T&&(右值引用)。

3.否则,假设e的类型是T。如果e是一个左值,则decltype(e)为T&(左值引用)。

4.否则,假设e的类型是T。则decltype(e)为T。

标记符表达式(id-expression):所有除了关键字、字面量等编译器需要使用的标记之外的程序员自己定义的标记(token)都可以是标记符(identifier)。单个标记符对应的表达式就是标记符表达式。例如 int arr[2]; 这里的arr就是标记符表达式,而arr[1]、arr[1] + 1都不是标记符表达式。


实例1

了解了推导规则后,我们来练习一个吧。

int main()
{
  int i;
  decltype(i) a;   // a type: int
  decltype((i)) b; // b type: int& 编译失败
  return 0;
}


先来分析decltype(i) a;i是一个标记符表达式,所以推导的类型就是实体的类型,也就是int

再来分析decltype((i)) b;(i)不是一个标记符表达式,所以第一条不成立,(i)是一个左值,所以符合规则第三条,那么推导的类型为int&

实例2

经过前面的简单练习,我相信应该对这个规则有一定的认知了。那么我们就在分析一个稍微复杂的程序。

定义下面变量以及函数,用于之后的分析:

  int i = 4;
  int arr[5] = { 0 };
  int* ptr = arr;
  struct S
  {
    double d;
    S():d(0){}
  }s;
  void Test();
  // 重载
  void Overloaded(int);
  void Overloaded(char);
  int&& RvalRef();
  const bool Func(int);


规则1 单个标记符表达式以及访问类成员变量 均为推导为原本类型

推导 说明
decltype(arr) var1; int[5] 标记符表达式
decltype(ptr) var2; int& 标记符表达式
decltype(Test) var3; void __cdecl(void) 标记符表达式
decltype(s.d) var4; double 成员访问表达式
decltype(Overloaded) var5; 编译失败 不支持重载版本的函数


规则2 将亡值 推导为类型的右值引用 &&

推导 说明
decltype(RvalRef()) var6 = 1; int&&


规则3 左值 推导为类型的引用

推导 说明
decltype(true ? i : 11) var7 = i; int& 三目运算符 这里会返回一个int类型
decltype((i)) var8 = i; int& 带括号的左值
decltype(++i) var9 = i; int& ++i 返回i的左值
decltype(arr[3]) var10 = i; int& []下标运算符会返回左值
decltype(*ptr) var11 = i; int& *ptr <=> arr[0] 返回左值
decltype(“lval”) var12 = “lval”; const char(&)[5] 字符串字面常量 属于左值


规则4 以上都不是,就推导为自身类型

推导 说明
decltype(1) var13; int 除了字符串外的字面常量均为右值
decltype(i++) var14; int i++返回右值
decltype(Func(i)) var15; const bool 返回右值


使用模板来辅助推导识别

我们可以通过C++11的is_lvalue_referenceis_rvalue_reference,可以帮助我们对一些推导结果的识别。

#include <type_traits>
#include <iostream>
int main()
{
  int i = 4;
  int arr[5] = { 0 };
  int* ptr = arr;
  int&& RvalRef();
  // 是右值引用吗?
  std::cout << std::is_rvalue_reference<decltype(RvalRef())>::value << std::endl;
  std::cout << std::is_rvalue_reference<decltype(i++)>::value << std::endl;
  // 是左值引用吗?
  std::cout << std::is_lvalue_reference<decltype(true ? i : i)>::value << std::endl;
  std::cout << std::is_lvalue_reference<decltype((i))>::value << std::endl;
  std::cout << std::is_lvalue_reference<decltype(i)>::value << std::endl;
  std::cout << std::is_lvalue_reference<decltype(++i)>::value << std::endl;
  std::cout << std::is_lvalue_reference<decltype(arr[0])>::value << std::endl;
  std::cout << std::is_lvalue_reference<decltype(*ptr)>::value << std::endl;
  std::cout << std::is_lvalue_reference<decltype("lval")>::value << std::endl;
  std::cout << std::is_lvalue_reference<decltype(i++)>::value << std::endl;
  return 0;
}


cv限定符的继承与冗余的符号

在定义变量/对象有被const和volatile限定符修饰,在使用decltype进行推导时,其成员不会继承const和volatile限定符。is_const用来判断是否被const限定符修饰,is_volatile用来判断是否被volatile修饰。

#include <type_traits>
#include <iostream>
using std::cout;
using std::endl;
using std::is_const;
using std::is_volatile;
int main()
{
  const int c = 0;
  volatile int v = 0;
  struct S
  {
    int i;
  };
  const S a = { 0 };
  volatile S b;
  volatile S* p = &b;
  cout << is_const<decltype(c)>::value << endl; // 1
  cout << is_volatile<decltype(v)>::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
  cout << is_volatile<decltype(p->i)>::value << endl;//0
  return 0;
}


decltype在表达式的推导中,如果遇到一个冗余的符号将会被优化掉。

#include <type_traits>
#include <iostream>
using std::cout;
using std::endl;
using std::is_lvalue_reference;
using std::is_rvalue_reference;
int main()
{
  int i = 1;
  int& j = i;
  int* p = &i;
  const int k = 1;
  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)* var4 = &p; // int**
  auto* v3 = p; // int*
  v3 = &i;
  const decltype(k) var5 = 1; // const冗余 将会被优化掉 
  return 0;
}


这里特别要注意的是decltype(p)*的情况。可以看到,在定义var4变量的时候,由于p的类型是int*,因此var3被定义为了int**类型。这跟auto声明中,·也可以是冗余的不同。在decltype后的*号,并不会被编译器忽略。


此外我们也可以看到,var4中 const可以被冗余的声明,这就会被优化掉,同样的volatilc限制符也会如此。


总的说来,decltype算得上是C++11中类型推导使用方式上最灵活的一种。虽然看起来它的推导规则比较复杂,有的时候跟auto推导结果还略有不同,但大多数时候,我们发现使用decltype还是自然而亲切的。一些细则的区别,读者可以在使用时遇到问题再返回查验。而下面的追踪返回类型的函数定义,则将融合auto、decltype,将C++11中的泛型能力提升到更高的水平。

目录
相关文章
|
2月前
|
存储 编译器 程序员
C++类型参数化
【10月更文挑战第1天】在 C++ 中,模板是实现类型参数化的主要工具,用于编写能处理多种数据类型的代码。模板分为函数模板和类模板。函数模板以 `template` 关键字定义,允许使用任意类型参数 `T`,并在调用时自动推导具体类型。类模板则定义泛型类,如动态数组,可在实例化时指定具体类型。模板还支持特化,为特定类型提供定制实现。模板在编译时实例化,需放置在头文件中以确保编译器可见。
37 11
|
3月前
|
安全 程序员 编译器
C++ 11新特性之auto和decltype
C++ 11新特性之auto和decltype
46 3
|
3月前
|
安全 程序员 C语言
C++(四)类型强转
本文详细介绍了C++中的四种类型强制转换:`static_cast`、`reinterpret_cast`、`const_cast`和`dynamic_cast`。每种转换都有其特定用途和适用场景,如`static_cast`用于相关类型间的显式转换,`reinterpret_cast`用于低层内存布局操作,`const_cast`用于添加或移除`const`限定符,而`dynamic_cast`则用于运行时的类型检查和转换。通过具体示例展示了如何正确使用这四种转换操作符,帮助开发者更好地理解和掌握C++中的类型转换机制。
|
4月前
|
C++
使用 QML 类型系统注册 C++ 类型
使用 QML 类型系统注册 C++ 类型
85 0
|
5月前
|
编译器 C++ 运维
开发与运维函数问题之函数的返回类型如何解决
开发与运维函数问题之函数的返回类型如何解决
39 6
|
5月前
|
安全 编译器 C++
C++一分钟之-模板元编程实例:类型 traits
【7月更文挑战第15天】C++的模板元编程利用编译时计算提升性能,类型traits是其中的关键,用于查询和修改类型信息。文章探讨了如何使用和避免过度复杂化、误用模板特化及依赖特定编译器的问题。示例展示了`is_same`类型trait的实现,用于检查类型相等。通过`add_pointer`和`remove_reference`等traits,可以构建更复杂的类型转换逻辑。类型traits增强了代码效率和安全性,是深入C++编程的必备工具。
93 12
|
4月前
|
存储 C++
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
51 0
|
5月前
|
C++
C++一分钟之-类型别名与using声明
【7月更文挑战第20天】在C++中,类型别名和`using`声明提升代码清晰度与管理。类型别名简化复杂类型,如`using ComplexType = std::vector&lt;std::shared_ptr&lt;int&gt;&gt;;`,需注意命名清晰与适度使用。`using`声明引入命名空间成员,避免`using namespace std;`全局污染,宜局部与具体引入,如`using math::pi;`。恰当应用增强代码质量,规避常见陷阱。
87 5
|
4月前
|
设计模式 安全 IDE
C++从静态类型到单例模式
C++从静态类型到单例模式
40 0
|
5月前
|
C++ 开发者
C++一分钟之-概念(concepts):C++20的类型约束
【7月更文挑战第4天】C++20引入了Concepts,提升模板编程的类型约束和可读性。概念定义了模板参数需遵循的规则。常见问题包括过度约束、约束不完整和重载决议复杂性。避免问题的关键在于适度约束、全面覆盖约束条件和理解重载决议。示例展示了如何用Concepts限制模板函数接受的类型。概念将增强模板的安全性和灵活性,但需谨慎使用以防止错误。随着C++的发展,Concepts将成为必备工具。
109 2