您现在的位置是:首页 >技术杂谈 >盘一盘C++的类型描述符(四)网站首页技术杂谈
盘一盘C++的类型描述符(四)
先序文章请看
盘一盘C++的类型描述符(三)
盘一盘C++的类型描述符(二)
盘一盘C++的类型描述符(一)
模板类型
模板类型不是类型
说到模板类型,我们必须要区分两个概念:
- 模板类型或偏特化模板类型
- 全特化模板类型
二者最大的区别在于「是不是模板」,所有模板形式的类型中,只有全特化模板它不是模板,而其他的则是模板。
关于模板概念、类型、特化等相关内容,请读者参考《C++模板元编程详细教程》这里不再赘述。
高清上面的概念以后,我们需要知道的是,本文所讨论的「C++类型」是不包括未实例化或未完全实例化的模板类型的,本质来说,模板类型根本就不是数据类型(它还没有成为类型)。举例来说:
template <typename T>
class Test {
};
对于上例来说,Test
是「模板类型」,而Test<int>
才是「类类型」,前者并不是数据类型。
既然模板类型不是真正的数据类型,那么这也就意味着,我们无法通过任何类型运算符获取到模板类型,这里的类型运算符包括decltype
、auto
,以及各种type_traits
工具比如说std::decay_t
。这个其实很好理解,看几个例子就明了了:
template <typename T>
class Test {
public:
Test(const T &t) {}
};
void Demo() {
using type1 = Test; // 非法
using type2 = Test<int>; // OK
Test t1 {2}; // 隐式实例化
Test<double> t2 {3};
using type3 = decltype(t1); // Test<int>,注意,并不是Test
using type4 = decltype(t2); // Test<double>
using type5 = std::decay_t<Test>; // 非法
using type6 = std::decay_t<Test<int>>; // OK
using type7 = std::decay_t<type3>; // OK
}
因此,能够参与类型运算的一定是数据类型,类型运算的结果也一定是数据类型,而模板类型并不是数据类型。模板类型只能出现在模板参数当中,这一点请读者一定要注意!
再来看一个很容易踩坑的例子:
template <typename T = int>
struct Test1 {};
struct Test2 {
Test1 t; // 报错
};
读者可以思考一下,上面代码为什么会报错?明明Test1
有指定默认参数呀!
这里我们需要明确,Test1
是模板,而Test1<int>
才是类型,所以,用一个没有实例化的模板类型,去定义变量自然是不合法的。那么如何体现,我们这里要用它的默认参数呢?加上尖括号就好了:
struct Test2 {
Test1<> t; // OK,等价于Test1<int> t
};
有的读者可能会有疑问,为什么我直接定义局部变量的时候可以?
void Demo() {
Test1 t; // OK
}
道理很简单,我们知道定义变量一定要用「类型」,所以,模板一定要实例化,那么这里其实就是隐式实例化了。而隐式实例化依赖于「构造参数」,在定义局部变量中,是会调用它默认构造函数的,因此,编译期由「默认构造函数(无参)」这个动作触发了「模板隐式实例化(空参)」,推导出了Test1<>
,再由于默认参数为int
,所以Test1<>
等价于Test<int>
。
但在定义成员变量的时候,是没有「构造」这个动作的(构造则发生在初始化列表中),仅仅只是需要类型,以确定成员的大小和偏移量(其实本质就是C语言中结构体的定义和处理方式)。既然都没有构造这个动作,也就不会进行隐式实例化了。
类型推导
最后我们来看看C++类型推导的两大难,它们是auto
和decltype
。
auto
先来说说auto
,我看到很多教程上各种强调类似于「auto
一般不会推出引用」,「auto
一般不会推出const
」等等这种方式的归纳总结,虽然完整详细,但着实会让读者一脸懵,而且很难记住。
因此,这里还是希望读者能够明白「语义」,我们了解它究竟是怎么样的表达方式,自然也就很容易推导出规律。
从语义上讲,auto
其实是一个「占位符」,用来充当变量类型的位置。所以,根据它出现的位置,本着「最简原则」即可。
比如说:
auto a = 5;
这里照理说,int
、const int
、const int &
、int &&
、const int &&
甚至是unsigned
、long
等都可以。但何为「最简」?自然是int
。那为什么不是long
呢?因为5
是int
类型,如果推导为long
则相当于进行了类型转换,所以它并不如int
简单。
「可明明5
是常量啊?为什么不认为是const
?」因为对于auto
来说,类型的「最简」是针对于其修饰的部分而言的。刚才我们说过,auto
就是个占位符,所以这里的语义是「定义一个变量a
,初始化为5
」,因此这个auto
显然是修饰a
的,而不是修饰5
的(5
只是定义变量a
的一个初始值罢了,或者可以理解为构造参数。)因此auto
并不会因为5
的这种「常量」性质而添加const
或右值引用。
或者我们用一种更简单粗暴的方式来解释,我们把auto
当做一个未知数,那么能让式子auto a = 5;
成立的auto
的最简表达式是什么?显然是int
。
再看几个例子:
int f1();
int &f2();
int &&f3();
const int f4();
const int &f5();
void Demo() {
int a = 5;
auto e1 = a;
auto e2 = std::move(a);
auto ef1 = f1();
auto ef2 = f2();
auto ef3 = f3();
auto ef4 = f4();
auto ef5 = f5();
}
请问这7个e
开头的变量都是什么类型?还是,用「式子能成立的最简类型」来分析,不难了解,所有的都是int
类型。
「那如果是复杂类型,这样做岂不是会复制?这样还算最简吗?」我相信这也是一些读者会产生的疑问,我们来看个例子:
struct Test {
// 假设这里面很复杂,变量很多,也可能是非可平凡复制的
};
Test f1();
Test &f2();
const Test &f3();
Test &&f4();
void Demo() {
Test t;
auto e1 = t;
auto e2 = std::move(t);
auto e3 = Test{};
auto ef1 = f1();
auto ef2 = f2();
auto ef3 = f3();
auto ef4 = f4();
}
如果它们的类型也都识别为Test
,那这里面除了e3
,ef1
和ef3
会触发C++17的CE(复制省略)以外,其他的都会触发拷贝构造或移动构造,从这个角度来解释,似乎推导为引用或const
引用才更简化?但真的是这样吗?
答案自然是否定的!因为auto
的类型推导是在「编译期」而不是「运行期」。这也很好理解,毕竟「类型」这个东西一定是编译期的概念,运行期就全变成机器指令的,在冯·诺依曼体系的计算机中,本来就是不区分指令段和数据段的,仅仅看你如何解释。
因此,auto
推导的所谓「最简」就是静态的,程序字面上的最简,是不会考虑运行时的情况的,这一点请读者悉知。
上面我们都是用int
为例,会更明显一些,如果是一些复杂类型,大家不要忘了我们前面章节讲过的方法,要看到哪些是修饰这个类型本身的,而那些外层间接的部分是无论如何都不会发生改变的,比如说:
int *const p;
auto a = p; // int *, 这里的const修饰的p本身,所以auto不会推导出。
// 上面的类似于:
const int v1 = 0;
auto e = v1; // int,而不是const int
const int *const p2;
auto e2 = p2; // const int *,注意识别解类型中的const,不会被消除
void (*pf)(int);
auto e3 = pf; // void (*)(int)
void (Test::*const pmf)(const int) const;
auto e4 = pmf; // void (Test::*)(const int) const,同理,函数原型中的const不会消除
我们再来看几个可能在意料之外,但分析后仍在情理之中的例子:
void f();
void Demo() {
auto e1 = f; // void (*)()
int arr[] {1, 2, 3};
auto e2 = arr; // int *
auto e3 = "abcd"; // const char *
}
别激动!我们一个一个来解释。首先,对于e1
来说,以一个函数来初始化,那么它只能是对应函数的函数指针类型。「为什么不是函数类型?」因为不存在函数类型的变量,所以能满足auto e1 = f;
式子的最简类型只能是void (*)()
,也就是void (*e1)() = f;
。
再来看看e2
,明明arr
是数组啊,为什么会推导出指针呢?也很简单,因为我们没法用一个数组来初始化另一个数组,类似于int e2[3] = arr;
这种显然是不合法的。但因为数组类型可以隐式转换为首元素的指针类型,所以,满足auto e2 = arr;
式子的最简类型只能是int *
,也就是int *e2 = arr;
。
那么e3
这个与e2
同理,单独的字符串字面量会被编译期处理为一个全局区的只读字符数组,也就是说"abcd"
的类型是const char[5]
(注意字符串末尾隐含的'