C++编译时的“变量”(下篇)
2015-09-30
上篇中介绍了C++的函数重载中的一些细节。我们可以通过函数重载实现编译时的“变量”。
但是我们暂时不提函数重载,从头开始思考“变量”的实现方式。
上篇的开头提到,C++在编译时相当于是immutable的——定义了的东西不能再修改。
那如何达到“可变”这个目标呢?答案并不复杂——事实上我们并不需要直接修改某个定义,而是把修改后的定义追加在已有的定义之后,形成一个链状结构,然后找到这条链中最新的那个值。
模板?
最容易想到的方式是利用模板特化来实现这个链状结构(嗯,请假装不知道这样做是有问题的):
1 template <int I>
2 struct Var {
3 // import the variable from Var<I + 1>
4 };
5
6 template <>
7 struct Var<100> {
8 // first version of the variable
9 };
10
11 template <>
12 struct Var<99> {
13 // second version of the variable
14 };
我们可以在其中放一个值,也可以放一个类型。一种方便的做法是把一个值用类型包装起来:
1 template <int V>
2 struct IntWrapper {
3 static constexpr int value = V;
4 };
5
6 template <int I>
7 struct Var {
8 using Content = typename Var<I + 1>::Content;
9 };
10 // or just "struct Var: public Var<I + 1> {};"
11
12 template <>
13 struct Var<100> {
14 using Content = IntWrapper<123>;
15 };
16
17 template <>
18 struct Var<99> {
19 using Content = IntWrapper<456>;
20 };
21
22 void test() {
23 std::cout << Var<0>::Content::value; // 456
24 }
直接使用Var<I + 1>::Content
是一种简单粗暴的做法。一个变量只能改100次,而且要倒数,这多少有些不妥。
我们再稍稍改变Var
的结构,去掉100的上限:
1 template <int I>
2 struct Var {
3 using Content = void;
4 };
5
6 template <>
7 struct Var<0> {
8 using Content = IntWrapper<123>;
9 };
10
11 template <>
12 struct Var<1> {
13 using Content = IntWrapper<456>;
14 };
如何取得值呢?引入一个FindValue
类(模板),进行递归搜索即可。
对于熟悉C++模板元编程的读者来说,这种做法应该并不陌生:
1 template <int I, class Current>
2 struct FindValue: public FindValue<
3 I + 1, typename Var<I + 1>::Content
4 > {};
5
6 template <int I>
7 struct FindValue<I, void>: public Var<I - 1>::Content {};
8
9 void test() {
10 std::cout << FindValue<0, Var<0>>::value;
11 }
虽然看上去利用模板的做法成功了,但这样做有个隐藏的致命问题。
C++模板有个“模板实例化”的过程。未实例化的模板被使用时,会自动地在使用位置前进行实例化。于是我们使用FindValue
时,Var
被实例化,然后就无法修改了。
这就像是薛定谔的猫,观测行为本身使观测结果坍缩了。
函数!
要避免这个问题,我们可以使用函数而非模板来存储这条链。
首先建立一系列函数。这些函数是在编译时用于类型推断的,因此只需声明,不必实现:
1 template <class T>
2 void var(T);
3
4 IntWrapper<123> var();
5 // or:
6 // struct VarInit {};
7 // IntWrapper<123> var(VarInit);
8
9 IntWrapper<456> var(IntWrapper<123>);
可以发现,链状结构是通过参数和返回值组织起来的——从IntWrapper<123>
到IntWrapper<456>
,再到void
。通过decltype(var(<上一个值>))
就能得到下一个值。
这里利用了C++的重载规则——有准确参数的函数优于函数模板,以及,寻找重载时只考虑之前出现的函数。
本文写到一半我才想起来,SFINAE在这里并不重要。其实SFINAE主要是用在了使用过“编译时变量”的、奇技淫巧满满的项目中的另一个地方……Anyway,不要在意细节。
用void var(...)
也是可以的,只需要使用IntWrapper<123> var(VarInit)
来区分两个重载。后文我们默认使用这种实现。
然后,递归搜索沿用之前的FindValue
,稍加修改就可以了:
1 template <class Last, class Current>
2 struct FindValue: public FindValue<
3 Current,
4 decltype(var(*static_cast<Current *>(nullptr)))
5 > {};
6
7 template <class Last>
8 struct FindValue<Last, void>: public Last {};
9
10 void test() {
11 std::cout << FindValue<void, decltype(var(VarInit))>::value;
12 }
需要第二次获取当前的var
值时,我们可以定义一个新的模板,比如叫FindValue1
。
这有个小问题:如何允许这个链中出现重复的值呢?对于“变量”而言,这是符合常识的。
解决方法是给IntWrapper
加上额外的模板参数,每次传入唯一的值,使IntWrapper
类变得唯一。这里可以简单地使用__COUNTER__
宏,也可以使用一个单独的类。
进一步地——声明新的var
函数重载时,参数是不必显式地写出来的。可以通过FindValue
得到:
1 IntWrapper<456> var(
2 FindValue<void, decltype(var(VarInit))>
3 );
虽然看上去这样会增加很多代码量,但由于代码几乎不变,把FindValue
定义成宏(当然,FindValue
的名字需要唯一化,方法很多,请读者自行思考相关奇技淫巧XD)即可。
本文所述的“编译时变量”出现于之前做的C++反射库reflection++中。不过,这个库中使用的并不是一个“变量”,而是“链”本身,用于存储一系列visitor类。
当然,这样做是实(娱)验(乐)性质的,因为它有两个无法避免的、工程上的硬伤:使头文件引入顺序能够影响程序本身的正确性,以及严重拖慢编译速度。