C++编译时的“变量”(下篇)

2015-09-30

C++, Meta, SFINAE

上篇中介绍了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类

当然,这样做是实(娱)验(乐)性质的,因为它有两个无法避免的、工程上的硬伤:使头文件引入顺序能够影响程序本身的正确性,以及严重拖慢编译速度。