分享
 
 
 

C++ 的MetaProgramming 入门篇

王朝c/c++·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

C++ 的另一个新世界

C++ 的 MetaProgramming

废话就不说了, 按照C的传统惯例,介绍programming的最好方式就是show代码, 第一个例子就是Hello,world, 这篇文章也不例外

在任何一个cpp文件中,输入

struct hello_world; //forward declaration

struct A : hello_world

{

};

然后编译..,注意我没有说"编译运行",而仅仅说的是"编译", 如果不意外的话,在你编译器的输出窗口会出现

base class hello_world undefined

或者

base class `hello_world' has incomplete type

等类似的语句, 至少在你的屏幕上打印出了 "hello_world" 字样,对吧? 对了,这就是这个例子的目的, 我不也打印出来的吗?

上面这个简陋的例子阐述了一个重要的现象, "编译时"而不是运行时, 这就是meta programming的世界, 一个处于编译期间, 而不是运行期间的世界.

运行时我们能作的事情很多在编译时我们不能作, 例如我们不能调用函数, 我们不能创建对象, 我们也不能设置断点, 一切都交给你的C++编译器.

接下来, 首先回顾一下一些C++的基本模板知识.

由于C++的编译器符合ISO 标准的程度不一, 我使用的是VC++ 6 和 gcc version 3.2.3 (mingw special 20030504-1)

下面的例子我在这两个编译器中都测试过.

C++模板的最经常的认识就是STL中的容器, 例如

vector<int> 就是一个可以装int的动态数组, vector<sharp*> 就是一个可以装sharp对象指针的数组.

然后我们还需要一点点模板特化的知识.

例如

template<class T> struct Foo {};

这是一个通用模板, 必配任何类型, 如果我们需要对int进行特别处理, 那么

template<> struct Foo<int> {};

这样就实现了对 int 类型的完全特化 . VC6 只支持完全特化, 不支持偏特化, 或者部分特化.

不过还是稍微介绍一下:

什么是遍特化了? 还就上面这个例子而言, 如果我们想对所有的指针进行特化, 那么应该是

template<class T> struct Foo<T*> {}

那么什么又是部分特化了, 看看这个:

template<class T, class U> struct bar {};

template<class T> struct bar<T, int> {};

后面这个就是对U模板参数的部分特化,使得 U为 int的时候 使用这个版本.

这篇文章由于针对VC6, 因此你不会遇到偏特化和部分特化的情况.

hello,world的例子以后, 我们准备干点有意思的事情. 例如求两数之和. 首先看看在运行时的函数我们应该如何实现:

定义:

int sum(int x, int y) { return x+y; }

调用:

int j = sum(3, 4)

那么如何用meta programming的方式实现了?

meta programming 由于处于编译器, 因此给它的参数必须是编译时就可以确定的, 在当前C++中允许的为,integer 和 type.

就上面这个例子, 对于模板的调用应该是:

int j = sum_<3,4>::value;

3,4 都为整形常量, 编译时确定, 然后返回值如何取得? 记住在编译时是无法进行真正的函数调用的,因此我们只有通过定义在模板类中的一个常量

来获得返回结果. 最简单的方法就是在一个 struct定义一个匿名的enum .

sum的定义如下:

template<int x, int y>

struct sum_

{

enum { value = x + y };

};

然后你可以编译后"运行"检查检查看看运行结果

#include <iostream>

using namespace std;

template<int x, int y>

struct sum_

{

enum { value = x + y };

};

int main()

{

cout << sum_<3, 4>::value << endl;

return 0;

}

上面之所以使用struct sum_而不是class sum_是因为struct的默认作用范围上public, 可以省略几个键击.

这个例子通用展示了一个重要的观点, 对应通常可以在运行时调用的函数

int foo(int a, int b)

其对应的meta 实现为:

foo<a, b>::value

也就是我们的foo现在为一个struct name, 参数通过< > 中的模板投递, 结果通过 ::value 获得其中定义的一个匿名enum值.

那么如何计算 3,4,5的和?

你可能会想如下:

sum_<3,4,5>::value

但是c++不支持一个模板类使用不同的参数参数存在, 换一个方式,

你如何在 int sum(int a, int b) 存在的情况下获得3个数的和?

我们可以这样:

sum( 3, sum(4, 5))

有lisp 背景的可能发现这很符合他们的思考方式, 至少以我浮浅的emacs lisp知识, 不使用中间变量. 同样,meta中你也可以这样使用:

sum_< //开始参数

sum_< 3 //第一个参数为3

sum_< 3, sum_< //第二个参数是另外一个表达式的结果

sum_< 3, sum_<4, //这个表达式的第一个参数为4

sum_< 3, sum_<4, 5> //这个表达式的第二个参数为5

sum_< 3, sum_<4, 5>::value //通过::value获得这个表达式的结果

sum_< 3, sum_<4, 5>::value >::value //然后获得整个表达式的结果

ok, 就这么多, 看出来没有, 再解释一次, 将上面我们的

cout << sum_<3, 4>::value << endl;

中4的位置用一个sum_ 替换就得到了我们需要的三个数之和.

cout << sum_<3, ?? >::value << endl;

===>

?? = sum_<4, 5>::value

===> then

cout << sum_<3, sum_<4, 5>::value >::value << endl;

如果我需要算 2, 3, 4, 5之和呢?

同样简单, 你将上面的3, 4 ,5 中的任何一个常量替换成对sum_ 进行一个调用就可以了.

例如:

out << sum_< ?? , sum_<4, 5>::value >::value << endl;

?? = sum_< 2, 3 >::value

合并后为

cout << sum_< sum_<2, 3>::value , sum_<4, 5>::value >::value << endl;

运行的结果为 14.

这就是meta中最简单的一个例子, 顺序调用, 如果你看明白了同时觉得有点意思的话, 下来我们讲讲

循环语句, 通常我们写程序都避免递归, 因为容易造成堆栈和大脑溢出,但是在 meta 中必须使用递归的方式.

下面看看一个计算"阶乘"的例子, 其实这个才真正是meta 中的hello,world.

先看后面, 我们调用的方式:

cout << fac<5>::value ; // 结果应该是 5 * 4 * 3 * 2 * 1 = 120;

通过前面的知识, 你知道fac是一个template struct的名字, 有一个模板参数int, value是其中的一个匿名枚举变量

于是你可以毫不犹豫的写下:

template<int i>

struct fac

{

enum { value = ??? };

};

但是在value的地方你卡住了, 如果根据 i 得到 value?

让我们将大脑从"编译时世界"切换到"运行时的世界", 你如何写一个通常的递归函数来计算阶乘?

int fac(int n)

{

if( n == 1)

return 1;

return n * fac(n-1);

}

注意 n == 1是一个递归退出条件,先不考虑n为1时的递归退出,

其他情况下是将n 乘以 fac (n - 1). 有了前面的sum_ 知识, 你应该可以推出 value = ???

中的??? 应该是

n * //n*

fac //调用下一个fac

fac<n-1> //参数为n-1

fac<n-1>::value //获得结果

因此fac "函数"的模板实现就是

template<int i>

struct fac

{

enum { value = i * fac<i - 1>::value };

};

然后我们再考虑递归退出条件, 为1时value应该为1, 拿起C++中的特化模板武器来

template<>

struct fac<1> //参数为1时的特化

{

enum { value = 1 };

};

这样就将参数为 1 时的返回值设置为1, 结束递归.

这样,当你输入

cout << fac<5>::value << endl;

时,编译器会实例化

template<>

struct fac<5>

{

enum { value = 5 * fac<4>::value };

};

由于fac<5> 需要 fac<4> , 因此然后实例化 fac<4>, 同样的原因, fac<3> , fac<2>, fac<1>

到fac<1> , 编译器发现了一个特化版本fac<1> 匹配, 在fac<1>中的value已经是一个常量, 不依赖其他的fac实例, 递归退出, 最后

我们得到最终结果120 .

easy, 对不?

然后再介绍 if语句.

还是上面的这个fac例子, 负数的阶乘是没有意义的,先不考虑数学问题,假设我们希望在这个情况下返回-1,如何实现?

如果我们输入fac<-2>::value , 那么编译器会尝试实例化fac<-3>, fac<-4>, ......

你发现这是一个无限递归条件, 在运行时会造成你堆栈溢出, 由于我们在"编译时世界", 取决于你编译器, 最终总有结束的时候.

例如VC6 报错: fatal error C1202: recursive type or function dependency context too complex

G++报错 : template instantiation depth exceeds maximum of 500

因此我们需要一个if判断语句, 发现当模板参数 < 1的时候返回 -1.

按照测试先行的原则, 我们可以预计

fac<-1>::value == -1 是成立的.

现在的问题是如何实现? 下次再说吧! 也给个机会折磨折磨你的大脑. 记住, 模板不仅仅可以通过enum返回整数, 还可以通过嵌套 typedef返回一个类型.

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有