您现在的位置是:首页 >技术教程 >盘一盘C++的类型描述符(五)网站首页技术教程

盘一盘C++的类型描述符(五)

borehole打洞哥 2023-06-16 16:00:02
简介盘一盘C++的类型描述符(五)

先序文章请看
盘一盘C++的类型描述符(四)
盘一盘C++的类型描述符(三)
盘一盘C++的类型描述符(二)
盘一盘C++的类型描述符(一)

类型推导

decltype

其实autodecltype完全就是两个世界的人,二者表达的语义完全不同。但因为它们都有「类型推导」的作用,因此总是有人会将它们混淆,它们也总是能被捆绑式提及。

前面我们q强调过,auto其实是一个占位符,我们可以把它理解成方程中的未知数,编译期会找出这个方程的最简解,然后替换auto,从而成为一个完整的表达式。

decltype则是一个编译期的运算符,用于求一个表达式的类型的,它就是非常简单粗暴地返回这个表达式的类型罢了,并没有过多的复杂规则。我们来看几个例子:

int f1();
int &f2();
const int f3();
const int &f4();
int &&f5();

void Demo() {
  int a = 5;
  int &r1 = a;
  const int &r2 = a;
  int &&r3 = std::move(a);

  using type1 = decltype(a); // int
  using type2 = decltype(&a); // int *
  using type3 = decltype(std::move(a)); // int &&
  using type4 = decltype(r1); // int &
  using type5 = decltype(r2); // const int &
  using type6 = decltype(r3); // int &&
  using type7 = decltype(f1); // int (), 函数类型
  using type8 = decltype(&f1); // int (*)(), 函数指针类型
  using type9 = decltype(f1()); // int, 因为有调用,所以这里取的是返回值类型
  using type10 = decltype(f2()); // int &
  using type11 = decltype(f3()); // int, 这里需要额外解释一下
  using type12 = decltype(f4()); // const int &
  using type13 = decltype(f5()); // int &&

  int arr[] {1, 2, 3};
  using type14 = decltype(arr); // int [3]
  using type15 = decltype(&arr); // int (*)[3]
  using type16 = decltype(arr[0]); // int &,注意下角标运算是引用传递

  using type17 = decltype("abc"); // const char(&) [4]
}

大多数的相信读者都没有问题,看例子就能明白。这里我需要额外解释三个,type7type11type17

首先我们来看一下type7,这是对一个函数本身进行decltype,所以得到的也应该是「函数」类型,所以我们要注意,如果你尝试拿函数类型来「定义变量」的话,得到的并不是变量,而是函数声明,比如说:

int f();

void Demo() {
  using type = decltype(f); // int ()类型
  type f2; // 相当于int f2(),所以这是一个函数声明
  // 如果要定义函数指针类型的变量,则需要使用对应的指针类型
  // 方法1:函数取地址后,推导出函数指针类型
  using type2 = decltype(&f); // int (*)()类型
  type2 pf; // 相当于int (*pf)(),所以这是定义了一个变量
  // 方法2:给函数类型加上指针(使用type_traits),变为对应的函数指针类型
  std::add_pointer_t<type> pf2; // 相当于int (*pf2)()
}

这一点希望读者明晰,因为decltype只会取表达式本身的类型,而不会进行任何额外处理,所以自然,对函数来说取的也是函数本身的类型。对数组类型也是同样的,会取出数组类型,而不是指针类型(就如上面的type14)。

我们再看看一下type11f3的函数原型是const int (),照理说返回值应该是const int,但为什么这里却推出了int呢?原因在于,我们看到这里函数返回没有引用符,所以一定是「值传递」,因此这就相当于对一个右值进行decltype。就像我们decltype(3)出来的也是int而不是const int一样,右值本来就不存在只读不只读的问题,它一定要由一个对象来承接,否则无意义(纯右值就会直接丢弃,将亡值就会立即析构),因此,函数返回值中的const是无意义的! 当然这里说的是类型本身的const,不是解类型、原身类型中的const

再看下面几个例子,我们来体会一下,函数返回值中无用的const

const int f1();
const int *f2();
int *const f3();
const int &f4();

void Demo() {
  using type1 = decltype(f1()); // int
  using type1 = decltype(f1()); // const int *
  using type1 = decltype(f1()); // int *
  using type1 = decltype(f1()); // const int &
}

再次强调,解类型、原身类型中的const不是修饰类型本身的,它不会被丢掉。

最后我们来看一下一开始的例子中的type17,篇幅隔得有点远,所以我再写一遍:

using type17 = decltype("abc"); // const char(&)[4]

首先,字符串字面量会被映射为不可变的字符数组,这个笔者已经提及多次了。那么加上字符串结尾隐藏的'',所以"abc"const char [4]这一点应该是毋庸置疑了,奇怪就奇怪在为什么这里会推出引用。

道理也很简单,对于独立出现的字符串字面量(而不是初始化字符数组时出现的)来说,编译期都会给一个单独的全局空间来存放,而如果程序中多个位置都使用了相同的字符串,那么编译期会把它们指向同一片空间,所以只需要全局存一份即可。为了验证这个说法,我们可以做一个实验:

void Demo() {
  auto s1 = "abc";
  auto s2 = "abc";
  std::printf("s1=%p
s2=%p
s1==s2 : %d", s1, s2, s1 == s2);
}

读者可以自行实验验证一下。对于相同的字符串字面量,编译期会只存一份,所以对应s1s2的值相同,指向了同一片内存空间。

这就是字符串字面量会推出引用的原因,因为"123"其实表达的是「全局区保存了{'1', '2', '3', ''}这一串数据的实体」,而不是「字符串的值」,因此字符串字面量是引用类型,decltype后也应该是引用类型。

decltype(auto)

相信autodecltype已经够我们喝一壶的了,结果万万没想到,这俩还能合体?!

那么这个decltype(auto)到底是个什么东西呢?我们再次强调一遍,auto是占位符,decltype是编译期运算符。所以相信大家应该能猜到了,decltype(auto)中的auto应该就是一个占位符了,而并不是真正要参与类型推导运算的内容。

注意!decltype(auto)这个语法只会出现在函数返回值中,其他地方出现都是不合法的。我们来看一个例子:

decltype(auto) f1() {
  return "123";
}

auto f2() {
  return "123";
}

void Demo() {
  using type1 = decltype(f1()); // const char(&)[4]
  using type2 = decltype(f2()); // const char *
}

这个例子可以非常好地说明问题,在f1中,decltype(auto)做了返回值,而我们说auto是个占位符,那么显然这里,auto其实代指的是返回值表达式,也就是return "123"这个表达式,所以我们翻译一下,其实这里应当是:

decltype("123") f1() {
  return "123";
}

那就显而易见了,decltype("123")的结果是const char (&)[4],所以这就是f1返回值的类型,下面的type1就可以验证。

而对于f2来说,auto在返回值类型的位置上,我们可以认为,把返回值当做一个临时变量,然后看它的类型,也就是:

auto f2() {
  auto ret = "123"; // 这里满足的是让式子成立的最简解,所以它是const char *
  return ret;
}

因此,autodecltype(auto)做函数返回值的时候,我们可以理解为,推导函数返回值的两种方法。auto则按照「满足返回值表达式的最简解」这种方式,而decltype(auto)则按照「返回值表达式类型本身」的这种方式。其实也就是auto推导跟decltype推导的区别了,希望读者不要被这种奇怪语法吓到。

总结

C++的类型说明符确实比较复杂,主要在于它除了继承了C语言原本就有点奇怪的说明方式以后,又加上了很多新特性,让整件事情就变得异常复杂了。

不过,当我们真正理解了「语义」以及说明方式的规律以后,困难也能够迎刃而解。

顺便在这里给大家提供一种验证某种类型是否符合预期的方式。例如我想验证一下,我推导出来的type是数组类型还是指针类型,那么可以这样来验证:

using type = decltype("abc");

// 我希望验证一下type到底是不是这些类型
std::cout << std::is_same_v<type, const char *> << std::endl; // 验证它是不是指针
std::cout << std::is_same_v<type, const char [4]> << std::endl; // 验证它是不是数组
std::cout << std::is_same_v<type, const char(&)[4]> << std::endl; // 验证它是不是引用

善用std::is_same来进行验证猜想。

[完结]

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。