随笔-341  评论-2670  文章-0  trackbacks-0
 
    复杂的东西写多了,如今写点简单的好了。由于功能上的需要,Vczh Library++3.0被我搞得很离谱。为了开发维护的遍历、减少粗心犯下的错误以及增强单元测试、回归测试和测试工具,因此记录下一些开发上的小技巧,以便抛砖引玉,造福他人。欢迎高手来喷,菜鸟膜拜。

    之前的文章讲了指针和内存的一些问题,今天说一下单元测试的问题。如果在团队里面没有对单元测试的框架有要求的话,其实我们可以使用一个最简单的方法来搭建在IDE里面运行的单元测试框架,整个框架只需十几行代码。我们先来考虑一下功能最少的单元测试框架需要完成什么样的内容。首先我们要运行一个一个的测试用例,其次在一个测试用例里面我们要检查一些条件是否成立。举个例子,我们写一个函数将两个字符串连接起来,一般来说要进行下面的测试:
 1 #include "MyUnitTestFramework.h"//等一下我们会展示一下如何用最少的代码完成这个头文件的内容
 2 #include ""
 3 
 4 TEST_CASE(StringConcat)
 5 {
 6   TEST_ASSERT(concat("a""b")=="ab");
 7   TEST_ASSERT(concat("a""")=="a");
 8   TEST_ASSERT(concat("""b")=="b");
 9   TEST_ASSERT(concat("""")=="");
10   .
11 }
12 
13 int wmain()
14 {
15   return 0;
16 }

    如果我们的单元测试框架可以这么写,那显然做起什么事情来都会方便很多,而且不需要向一些其他的测试框架一样注册一大堆东西,或者是写一大堆配置函数。当然这次我们只做功能最少的测试框架,这个框架除了运行测试以外,不会有其他功能,譬如选择哪些测试可以运行啦,还是在出错的时候log一些什么啦之类。之所以要在IDE里面运行,是因为我们如果做到TEST_ASSERT中出现false的话,立刻在该行崩溃,那么IDE就会帮你定位到出错的TEST_ASSERT中去,然后给你显示所有的上下文信息,譬如说callstack啦什么的。友好的工具不用简直对不起自己啊,干吗非得把单元测试做得那么复杂捏,凡是单元测试,总是要全部运行通过才能提交代码的。

    那么我们来看看上面的单元测试的代码。首先写了TEST_CASE的那个地方,大括号里面的代码会自动运行。其次TEST_ASSERT会在表达式是false的时候崩溃。先从简单的入手吧。如何制造崩溃呢?最简单的办法就是抛异常:
1 #define TEST_ASSERT(e) do(if(!(e))throw "今晚没饭吃。";}while(0)

    这里面有两个要注意的地方。首先e要加上小括号,不然取反操作符就有可能做出错误的行为。譬如说当e是a+b==c的时候,加了小括号就变成if(!(a+b==c))...,没有加小括号就变成if(!a+b==c)...,意思就完全变了。第二个主意的地方是我使用do{...}while(0)把语句包围起来了。这样做的好处是可以在任何时候TEST_ASSERT(e)都像一个语句。譬如我们可能这么写:
1 if(a)
2   TEST_ASSERT(x1);
3 else if(b)
4 {
5   TEST_ASSERT(x2);
6   TEST_ASSERT(x3);
7 }

    如果没有do{...}while(0)包围起来,这个else就会被绑定到宏里面的那个if,你的代码就被偷偷改掉了。

    那么现在剩下TEST_CASE(x){y}了。什么东西可以在main函数外面自动运行呢?这个我想熟悉C++的人都会知道,就是全局变量的构造函数啦。所以TEST_CASE(x){y}那个大括号里面的y只能在全局变量的构造函数里面调用。但是我们知道写一个类的时候,构造函数的大括号写完了,后面还有类的大括号,全局变量的名称,和最终的一个分号。为了把这些去掉,那么显然{y}应该属于一个普通的函数。那么全局变量如何能够使用这个函数呢?方法很简单,把函数前置声明一下就行了:
 1 #define TEST_CASE(NAME)                                            \
 2         extern void TESTCASE_##NAME();                             \
 3         namespace vl_unittest_executors                            \
 4         {                                                          \
 5             class TESTCASE_RUNNER_##NAME                           \
 6             {                                                      \
 7             public:                                                \
 8                 TESTCASE_RUNNER_##NAME()                           \
 9                 {                                                  \
10                     TESTCASE_##NAME();                             \
11                 }                                                  \
12             } TESTCASE_RUNNER_##NAME##_INSTANCE;                   \
13         }                                                          \
14         void TESTCASE_##NAME()

    那我们来看看TEST_CASE(x){y}究竟会被翻译成什么代码:
 1 extern void TESTCASE_x();
 2 namespace vl_unittest_executors
 3 {
 4     class TESTCASE_RUNNER_x
 5     {
 6     public:
 7         TESTCASE_RUNNER_x()
 8         {
 9             TESTCASE_x();
10         }
11     } TESTCASE_RUNNER_x_INSTANCE;
12 }
13 void TESTCASE_x(){y}

    到了这里是不是很清楚了捏,首先在main函数运行之前TESTCASE_RUNNER_x_INSTANCE变量会初始化,然后调用TESTCASE_RUNNER_x的构造函数,最后运行函数TESTCASE_x,该函数的内容显然就是{y}了。这里还能学到宏是如何连接两个名字成为一个名字,和如何写多行的宏的。

    于是MyUnittestFramework.h就包含这两个宏,其他啥都没有,是不是很方便呢?打开Visual C++,建立一个工程,引用这个头文件,然后写你的单元测试,最后F5就运行了,多方便啊,啊哈哈哈。

    这里需要注意一点,那些单元测试的顺序是不受到保证的,特别是你使用了多个cpp文件的情况下。于是你在使用这个测试框架的同时,会被迫保证执行一次单元测试不会对你的全局状态带来什么副作用,以便两个测试用例交换顺序执行的时候仍然能稳定地产生相同的结果。这对你写单元测试有帮助,而且为了让你的代码能够被这么测试,你的代码也会写的有条理,不会依赖全局状态,真是一举两得也。而且说不定单元测试用例比你的全局变量的初始化还先执行呢,因此为了使用这个测试框架,你将会不得不把你的全局变量隐藏在一个cpp里面,而暴露出随时可以被调用的一组函数出来。这样也可以让你的代码在使用全局状态的时候更加安全。

    今天就讲到这里了。下一篇要写什么我还没想好,到时候再说吧。
posted @ 2010-06-27 04:19 陈梓瀚(vczh) 阅读(10002) | 评论 (16)编辑 收藏

    复杂的东西写多了,如今写点简单的好了。由于功能上的需要,Vczh Library++3.0被我搞得很离谱。为了开发维护的遍历、减少粗心犯下的错误以及增强单元测试、回归测试和测试工具,因此记录下一些开发上的小技巧,以便抛砖引玉,造福他人。欢迎高手来喷,菜鸟膜拜。

    今天是关于内存的最后一篇了。上一篇文章讲了为什么不能对一个东西随便memset。里面的demo代码出了点小bug,不过我不喜欢在发文章的时候里面的demo代码也拿去编译和运行,所以大家有什么发现的问题就评论吧。这样也便于后来的人不会受到误导。这次说的仍然是构造函数和析构函数的事情,不过我们将通过亲手开发一个智能指针的方法,知道引用计数如何帮助管理资源,以及错误使用引用计数的情况。

    首先先来看一下智能指针是如何帮助我们管理内存的。现在智能指针的实现非常多,我就假设这个类型叫Ptr<T>吧。这跟Vczh Library++ 3.0所使用的实现一样。

 1 class Base
 2 {
 3 public:
 4   virtual ~Base(){}
 5 };
 6 
 7 class Derived1 : public Base
 8 {
 9 };
10 
11 class Derived2 : public Base
12 {
13 };
14 
15 //---------------------------------------
16 
17 List<Ptr<Base>> objects;
18 objects.Add(new Derived1);
19 objects.Add(new Derived2);
20 
21 List<Ptr<Base>> objects2;
22 objects2.Add(objects[0]);

    当然这里的List也是Vczh Library++3.0实现的,不过这玩意儿跟vector也好跟C#的List也好都是一个概念,因此也就不需要多加解释了。我们可以看到智能指针的一个好处,只要没有循环引用出现,你无论怎么复制它,最终总是可以被析构掉的。另一个例子告诉我们智能指针如何处理类型转换:
1 Ptr<Derived1> d1=new Derived1;
2 Ptr<Base> b=d1;
3 Ptr<Derived2> d2=b.Cast<Derived2>();
4 // d2是空,因为b指向的是Derived1而不是Derived2。

    这就如同我们Derived1*可以隐式转换到Base*,而当你使用dynamic_cast<Derived2*>(static_cast<Base*>(new Derived1))会得到0一样。智能指针在帮助我们析构对象的同时,也要做好类型转换的工作。

    好了,现在先让我们一步一步做出那个Ptr<T>。我们需要清楚这个智能指针所要实现的功能是什么,然后我们一个一个来做。首先让我们列出一张表:
    1、没有参数构造的时候,初始化为空
    2、使用指针构造的时候,拥有那个指针,并且在没有任何智能指针指向那个指针的时候删除掉该指针。
    3、智能指针进行复制的时候,两个智能指针共同拥有该内部指针。
    4、智能指针可以使用新的智能指针或裸指针重新赋值。
    5、需要支持隐式指针类型转换,static_cast不支持而dynamic_cast支持的转换则使用Cast<T2>()成员函数来解决。
    6、如果一个裸指针直接用来创建两个智能指针的话,期望的情况是当两个智能指针析构掉的时候,该指针会被delete两次从而崩溃。
    7、不处理循环引用。

    最后两点实际上是错误使用智能指针的最常见的两种情况。我们从1到5一个一个实现。首先是1。智能指针可以隐式转换成bool,可以通过operator->()拿到内部的T*。在没有使用参数构造的时候,需要转换成false,以及拿到0:
 1 template<typename T>
 2 class Ptr
 3 {
 4 private:
 5   T* pointer;
 6   int* counter;
 7 
 8   void Increase()
 9   {
10     if(counter)++*counter;
11   }
12 
13   void Decrease()
14   {
15     if(counter && --*counter==0)
16     {
17       delete counter;
18       delete pointer;
19       counter=0;
20       pointer=0;
21     }
22   }
23 
24 public:
25   Ptr():pointer(0),counter(0)
26   {
27   }
28 
29   ~Ptr()
30   {
31     Decrease();
32   }
33 
34   operator bool()const
35   {
36     return counter!=0;
37   }
38 
39   T* operator->()const
40   {
41     return pointer;
42   }
43 };

    在这里我们实现了构造函数和析构函数。构造函数把内部指针和引用计数的指针都初始化为空,而析构函数则进行引用计数的减一操作。另外两个操作符重载很容易理解。我们主要来看看Increase函数和Decrease函数都分别做了什么。Increase函数在引用计数存在的情况下,把引用计数加一。而Decrease函数在引用计数存在的情况下,把引用计数减一,如果引用计数在减一过程中变成了0,则删掉拥有的资源。

    当然到了这个时候智能指针还不能用,我们必须替他加上复制构造函数,operator=操作符重载以及使用指针赋值的情况。首先让我们来看使用指针赋值的话我们应该加上什么:
 1   Ptr(T* p):pointer(0),counter(0)
 2   {
 3     *this=p;
 4   }
 5 
 6   Ptr<T>& operator=(T* p)
 7   {
 8     Decrease();
 9     if(p)
10     {
11       pointer=p;
12       counter=new int(1);
13     }
14     else
15     {
16       pointer=0;
17       counter=0;
18     }
19     return *this;
20   }

    这里还是偷工减料了的,构造函数接受了指针的话,还是转给operator=去调用了。当一个智能指针被一个新指针赋值的时候,我们首先要减掉一个引用计数,因为原来的指针再也不被这个智能指针共享了。之后就进行判断,如果来的是0,那么就变成空。如果不是0,就拥有该指针,引用计数初始化成1。于是我们就可以这么使用了:
1 Ptr<Base> b=new Derived1;
2 Ptr<Derived2> d2=new Derived2;

    让我们开始复制他们吧。复制的要领是,先把之前拥有的指针脱离掉,然后连接到一个新的智能指针上面去。我们知道非空智能指针有多少个,总的引用计数的和就是多少,只是分配到各个指针上面的数字不一样而已:
 1   Ptr(const Ptr<T>& p):pointer(p.pointer),counter(p.counter)
 2   {
 3     Increase();
 4   }
 5 
 6   Ptr<T>& operator=(const Ptr<T>& p)
 7   {
 8     if(this!=&p)
 9     {
10       Decrease();
11       pointer=p.pointer;
12       counter=p.counter;
13       Increase();
14     }
15     return *this;
16   }

    在上一篇文章有朋友指出重载operator=的时候需要考虑是不是自己赋值给自己,其实这是很正确的。我们写每一类的时候,特别是当类拥有自己控制的资源的时候,需要非常注意这件事情。当然如果只是复制几个对象而不会new啊delete还是close什么handle,那检查不检查也无所谓了。在这里我们非常清楚,当增加一个新的非空智能指针的时候,引用计数的总和会加一。当修改一个非空智能指针的结果也是非空的时候,引用计数的和保持不变。当然这是应该的,因为我们需要在所有非空智能指针都被毁掉的时候,释放受保护的所有资源。

    到了这里一个智能指针基本上已经能用了,但是还不能处理父类子类的情况。这个是比较麻烦的,一个Ptr<Derived>事实上没有权限访问Ptr<Base>的内部对象。因此我们需要通过友元类来解决这个问题。现在让我们来添加两个新的函数吧,从一个任意的Ptr<C>复制到Ptr<T>,然后保证只有当C*可以隐式转换成T*的时候编译能够通过:
 1   template<X> friend class Ptr;
 2 
 3   template<typename C>
 4   Ptr(const Ptr<C>& p):pointer(p.pointer),counter(p.counter)
 5   {
 6     Increase();
 7   }
 8 
 9   template<typename C>
10   Ptr<T>& operator=(const Ptr<C>& p)
11   {
12     Decrease();
13     pointer=p.pointer;
14     counter=p.counter;
15     Increase();
16     return *this;
17   }

    注意这里我们的operator=并不用检查是不是自己给自己赋值,因为这是两个不同的类,相同的话会调用上面那个operator=的。如果C*不能隐式转换到T*的话,这里的pointer=p.pointer就会失败,从而满足了我们的要求。

    现在我们能够做的事情就更多了:
1 Ptr<Derived1> d1=new Derived1;
2 Ptr<Base> b=d1;

    于是我们只剩下最后一个Cast函数了。这个函数内部使用dynamic_cast来做判断,如果转换失败,会返回空指针:
 1   tempalte<typename C>
 2   Ptr<C> Cast()const
 3   {
 4     C* converted=dynamic_cast<C*>(pointer);
 5     Ptr<C> result;
 6     if(converted)
 7     {
 8       result.pointer=converted;
 9       result.counter=counter;
10       Increase();
11     }
12     return result;
13   }

    这是一种hack的方法,平时是不鼓励的……不过因为操作的都是Ptr,而且特化Ptr也是使用错误的一种,所以这里就不管了。我们会检查dynamic_cast的结果,如果成功了,那么会返回一个非空的新智能指针,而且这个时候我们也要记住Increase一下。

    好了,基本功能就完成了。当然一个智能指针还要很多其他功能,譬如说比较什么的,这个就你们自己搞定哈。

    指针和内存就说到这里了,下一篇讲如何利用一个好的IDE构造轻量级单元测试系统。我们都说好的工具能够提高生产力,因此这种方法不能脱离一个好的IDE使用。
posted @ 2010-06-23 23:03 陈梓瀚(vczh) 阅读(9776) | 评论 (15)编辑 收藏
    复杂的东西写多了,如今写点简单的好了。由于功能上的需要,Vczh Library++3.0被我搞得很离谱。为了开发维护的遍历、减少粗心犯下的错误以及增强单元测试、回归测试和测试工具,因此记录下一些开发上的小技巧,以便抛砖引玉,造福他人。欢迎高手来喷,菜鸟膜拜。

    上一篇文章讲到了如何检查内存泄露。其实只要肯用C++的STL里面的高级功能的话,内存泄露是很容易避免的。我在开发Vczh Library++ 3.0的时候,所有的测试用例都保证跑完了没有内存泄露。但是很可惜有些C++团队不能使用异常,更甚者不允许写构造函数析构函数之类,前一个还好,后一个简直就是在用C。当然有这些变态规定的地方STL都是用不了的,所以我们更加需要扎实的基础来开发C++程序。

    今天这一篇主要还是讲指针的问题。因为上一篇文章一笔带过,今天就来详细讲内存泄漏或者野指针发生的各种情况。当然我不可能一下子举出全部的例子,只能说一些常见的。

    一、错误覆盖内存。

    之前提到的不能随便乱memset其实就是为了避免这个问题的。其实memcpy也不能乱用,我们来看一个例子,最简单的:
 1 #define MAX_STRING 20;
 2 
 3 struct Student
 4 {
 5   char name[MAX_STRING];
 6   char id[MAX_STRING];
 7   int chinese;
 8   int math;
 9   int english;
10 };

    大家对这种结构肯定十分熟悉,毕竟是大学时候经常要写的作业题……好了,大家很容易看得出来这其实是C语言的经典写法。我们拿到手之后,一般会先初始化一下,然后赋值。
1 Student vczh;
2 memset(&vczh, 0sizeof(vczh));
3 strcpy(vczh.name, "vczh");
4 strcpy(vczh.id, "VCZH'S ID");
5 vczh.chinese=70;
6 vczh.math=90;
7 vczh.english=80;

    为什么要在这里使用memset呢?memset的用处是将一段内存的每一个字节都设置成同一个数字。这里是0,因此两个字符串成员的所有字节都会变成0。因此在memset了Student之后,我们通过正常方法来访问name和id的时候都会得到空串。而且如果Student里面有指针的话,0指针代表的是没有指向任何有效对象,因此这个时候对指针指向的对象进行读写就会立刻崩溃。对于其他数值,0一般作为初始值也不会有什么问题(double什么的要小心)。这就是我们写程序的时候使用memset的原因。

    好了,如今社会进步,人民当家做主了,死程们再也不需要受到可恶的C语言剥削了,我们可以使用C++!因此我们借助STL的力量把Student改写成下面这种带有C++味道的形式:
1 struct Student
2 {
3   std::string name;
4   std::string id;
5   int chinese;
6   int math;
7   int english;
8 };

    我们仍然需要对Student进行初始化,不然三个分数还是随机值。但是我们又不想每一次创建的时候都对他们分别进行赋值初始化城0。这个时候你心里可能还是想着memset,这就错了!在memset的时候,你会把std::string内部的不知道什么东西也给memset掉。假如一个空的std::string里面存放的指针指向的是一个空的字符串而不是用0来代表空的时候,一下子内部的指针就被你刷成0,等下std::string的析构函数就没办法delete掉指针了,于是内存泄露就出现了。有些朋友可能不知道上面那句话说的是什么意思,我们现在来模拟一下不能memset的std::string要怎么实现。

    为了让memset一定出现内存泄露,那么std::string里面的指针必须永远都指向一个有效的东西。当然我们还需要在字符串进行复制的时候复制指针。我们这里不考虑各种优化技术,用最简单的方法做一个字符串出来:
 1 class String
 2 {
 3 private:
 4   char* buffer;
 5 
 6 public:
 7   String()
 8   {
 9     buffer=new char[1];
10     buffer[0]=0;
11   }
12 
13   String(const char* s)
14   {
15     buffer=new char[strlen(s)+1];
16     strcpy(buffer, s);
17   }
18 
19   String(const String& s)
20   {
21     buffer=new char[strlen(s.buffer)+1];
22     strcpy(buffer, s.buffer);
23   }
24 
25   ~String()
26   {
27     delete[] buffer;
28   }
29 
30   String& operator=(const String& s)
31   {
32     delete[] buffer;
33     buffer=new char[strlen(s.buffer)+1];
34     strcpy(buffer, s.buffer);
35   }
36 };

    于是我们来做一下memset。首先定义一个字符串变量,其次memset掉,让我们看看会发生什么事情:
1 string s;
2 memset(&s, 0sizeof(s));

    第一行我们构造了一个字符串s。这个时候字符串的构造函数就会开始运行,因此strcmp(s.buffer, "")==0。第二行我们把那个字符串给memset掉了。这个时候s.buffer==0。于是函数结束了,字符串的析构函数尝试delete这个指针。我们知道delete一个0是不会有问题的,因此程序不会发生错误。我们活生生把构造函数赋值给buffer的new char[1]给丢了!铁定发生内存泄露!

    好了,提出问题总要解决问题,我们不使用memset的话,怎么初始化Student呢?这个十分好做,我们只需要为Student加上构造函数即可:
1 struct Student
2 {
3   .//不重复那些声明
4 
5   Student():chinese(0),math(0),english(0)
6   {
7   }
8 };

    这样就容易多了。每当我们定义一个Student变量的时候,所有的成员都初始化好了。name和id因为string的构造函数也自己初始化了,因此所有的成员也都初始化了。加入Student用了一半我们想再初始化一下怎么办呢?也很容易:
1 Student vczh;
2 .//各种使用
3 vczh=Student();

    经过一个等号操作符的调用,旧Student的所有成员就被一个新的初始化过的Student给覆盖了,就如同我们对一个int变量重新赋值一样常见。当然因为各种复制经常会出现,因此我们也要跟上面贴出来的string的例子一样,实现好那4个函数。至此我十分不理解为什么某些团队不允许使用构造函数,我猜就是为了可以memset,其实是很没道理的。

    二、异常。

    咋一看内存泄露跟异常好像没什么关系,但实际上这种情况更容易发生。我们来看一个例子:
 1 char* strA=new char[MAX_PATH];
 2 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
 3 char* strB=new char[MAX_PATH];
 4 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
 5 
 6 DoSomething(strA, strB);
 7 
 8 RELEASE_STRB:
 9 delete[] strB;
10 RELEASE_STRA:
11 delete[] strA;

    相信这肯定是大家的常用模式。我在这里也不是教唆大家使用goto,不过对于这种例子来说,用goto是最优美的解决办法了。但是大家可以看出来,我们用的是C++,因为这里有new。如果DoSomething发生了异常怎么办呢?如果GetXXX发生了异常怎么办呢?我们这里没有任何的try-catch,一有异常,函数里克结束,两行可怜的delete就不会被执行到了,于是内存泄漏发生了

    那我们如何避免这种情况下的内存泄露呢?一些可爱的小盆友可能会想到,既然是因为没有catch异常才发生的内存泄露,那我们来catch吧:
 1 char* strA=new char[MAX_PATH];
 2 try
 3 {
 4   if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
 5   char* strB=new char[MAX_PATH];
 6   try
 7   {
 8     if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
 9     DoSomething(strA, strB);
10   }
11   catch()
12   {
13     delete[] strB;
14     throw;
15   }
16 }
17 catch()
18 {
19   delete[] strA;
20   throw;
21 }
22 
23 RELEASE_STRB:
24 delete[] strB;
25 RELEASE_STRA:
26 delete[] strA;

    你能接受吗?当然是不能的。问题出在哪里呢?因为C++没有try-finally。你看这些代码到处都是雷同的东西,显然我们需要编译器帮我们把这些问题搞定。最好的解决方法是什么呢?显然还是构造函数和析构函数。总之记住,如果想要事情成对发生,那么使用构造函数和析构函数

    第一步,GetXXX显然只能支持C模式的东西,因此我们要写一个支持C++的:
 1 bool GetXXX2(string& s)
 2 {
 3   char* str=new char[MAX_PATH];
 4   bool result;
 5   try
 6   {
 7     result=GetXXX(str, MAX_PATH);
 8     if(result)s=str;
 9   }
10   catch()
11   {
12     delete[] str;
13     throw;
14   }
15   delete[] str;
16   return result;
17 }

    借助这个函数我们可以看到,因为有了GetXXX这种C的东西,导致我们多了多少麻烦。不过这总是一劳永逸的,有了GetXXX2和修改之后的DoSomething2之后,我们就可以用更简单的方法来做了:
1 string a,b;
2 if(GetXXX2(a) && GetXXX2(b))
3 {
4   DoSomething2(a, b);
5 }

    多么简单易懂。这个代码在任何地方发生了异常,所有new的东西都会被delete。这就是析构函数的一个好处。一个变量的析构函数在这个变量超出了作用域的时候一定会被调用,无论代码是怎么走出去的。

    今天就说到这里了。说了这么多还是想让大家不要小看构造函数和析构函数。那种微不足道的因为一小部分不是瓶颈的性能问题而放弃构造函数和析构函数的做法,终究是要为了修bug而加班的。只要明白并用好了构造函数、析构函数和异常,那么C++的特性也可以跟C一样清楚明白便于理解,而且写出来的代码更好看的。大家期待第三篇哈。
posted @ 2010-06-23 10:12 陈梓瀚(vczh) 阅读(11633) | 评论 (23)编辑 收藏
    复杂的东西写多了,如今写点简单的好了。由于功能上的需要,Vczh Library++3.0被我搞得很离谱。为了开发维护的遍历、减少粗心犯下的错误以及增强单元测试、回归测试和测试工具,因此记录下一些开发上的小技巧,以便抛砖引玉,造福他人。欢迎高手来喷,菜鸟膜拜。

    C++实谓各种语言中的软肋,功能强大,陷阱更强大。当然我认为一门语言用得不好完全是程序员的责任,不过因为C++涉及到的概念实在是太多,想用好实在也不是一件容易的事情。C++开发的时候总是会遇到各种各样的问题,其中最严重的无非是内存相关的。C语言由于结构简单,内存处理起来虽然不得力,但总的来说惯用法已经深入人心,因此也不会造成什么很难发现的错误。C++就不一样了。有了虚函数、构造函数、析构函数、复制构造函数和operator=重载之后,还是有很多人喜欢把一个类直接写进文件流,或者拿来memset,代码一团乱麻,不知悔改也。但是不能因此因噎废食,就像某人因为C++带来的心智问题太多,自己搞不定,自己团队也搞不定,就说C++不好一样。

    因此第一篇文章主要针对内存来讲。我们处理内存,第一件事就是不要有内存泄露。内存泄露不能等到测试的时候,通过长时间运行程序并观察任务管理器的方法来做,这显然已经晚了。幸好Visual C++给了我们一个十分好用的工具:_CrtDumpMemoryLeaks函数。这个函数会在Debug模式下往Visual Studio的output窗口打印出那个时候你new(malloc)了但是还没delete(free)的所有内存块的地址、长度、前N个字节的内容和其他信息。怎么做呢?其实很简单:
 1 #define _CRTDBG_MAP_ALLOC
 2 #include <stdlib.h>
 3 #include <crtdbg.h>
 4 #include <windows.h>
 5 
 6 int wmain(vint argc , wchar_t* args[])
 7 {
 8     // 这里运行程序,并在下面的函数调用之前delete掉所有new的东西
 9     _CrtDumpMemoryLeaks();
10     return 0;
11 }

    我们只需要在注释的地方完成我们程序的功能,然后确信自己已经delete掉所有应该delete的东西,最后_CrtDumpMemoryLeaks()函数调用的时候就可以打印出没被delete的东西了。这个方法十分神奇,因为你只需要在main函数所在的cpp文件这么#include一下,所有的cpp文件里面的new都会受到监视,跟平常所用的用宏把new给换掉的这种破方法截然不同。如果你使用了全局变量的话也要小心,因为全局变量的析构函数是在main函数结束之后才执行的,因此如果在全局变量的析构函数里面delete的东西仍然会被_CrtDumpMemoryLeaks函数当成泄露掉的资源对待。当然本人认为全局变量可以用,但是全局变量的赋值必须在main里面做,释放也是,除非那个全局变量的构造函数没有申请任何内存,所以这也是一个很好的检查方法。

    不过上面也仅仅是一个告诉你有没有内存泄漏的方法罢了。那么如何避免内存泄露呢?当然在设计一些性能要求没有比操作系统更加严格的程序的时候,可以使用以下方法:
    1、如果构造函数new了一个对象并使用成员指针变量保存的话,那么必须在析构函数delete它,并且不能有为了某些便利而将这个对象的所有权转让出去的事情发生。
    2、在能使用shared_ptr的时候,尽量使用shared_ptr。shared_ptr只要你不发生循环引用,那么这个东西可以安全地互相传递、随便你放在什么容器里面添加删除、你想放哪里就放在哪里,再也不用考虑这个对象的生命周期问题了。
    3、不要在有构造函数和析构函数的对象上使用memset(或者memcpy)。如果一个对象需要memset,那么在该对象的构造函数里面memset自己。如果你需要memset一个对象数组,那也在该对象的构造函数里面memset自己。如果你需要memset一个没有构造函数的复杂对象,那么请为他添加一个构造函数,除非那是别人的API提供的东西
    4、如果一个对象是继承了其他东西,或者某些成员被标记了virtual的话,绝对不要memset。对象是独立的,也就是说父类内部结构的演变不需要对子类负责。哪天父类里面加了一个string成员,被子类一memset,就欲哭无泪了。
    5、如果需要为一个对象定义构造函数,那么连复制构造函数、operator=重载和析构函数都全部写全。如果不想写复制构造函数和operator=的话,那么用一个空的实现写在private里面,确保任何试图调用这些函数的代码都出现编译错误。
    6、如果你实在很喜欢C语言的话,那麻烦换一个只支持C不支持C++的编译器,全面杜绝因为误用了C++而导致你的C坏掉的情况出现。

    什么是循环引用呢?如果两个对象互相使用一个shared_ptr成员变量直接或者间接指向对方的话,就是循环引用了。在这种情况下引用计数会失效,因为就算外边的shared_ptr全释放光了,引用计数也不会是0的。

    今天就说到这里了,过几天我高兴的话再写一篇续集,如果我持续高兴的话呢……嗯嗯……。
posted @ 2010-06-22 05:16 陈梓瀚(vczh) 阅读(37870) | 评论 (32)编辑 收藏
    大概都是要定稿了罢,想了这么久……前人果然是前人啊,C++的concept也好,Haskell的type class也好,C#的generic interface也好,都非常精确地描述出了NativeX的泛型所应该有的形式。设计语言什么的,还是大部分要抄啊……

    接上一篇文章。昨天晚上Vczh Library++的泛型结构体以及泛型类型重命名已经搞定了。这部分先做是因为泛型结构体以及泛型类型重命名都不需要在链接的时候产生新的指令表,因此完全是编译器的事情,不需要修改虚拟机。先来看看泛型结构体以及泛型类型重命名的样子。这次我仍然在单元测试用例里面生成了一个语法树,然后反编译成NativeX代码,然后再一次编译成语法树,最后给生成的指令自动加注释:
 1 /*NativeX Code*/
 2 unit nativex_program_generated;
 3 generic<T>
 4 type Unit = T;
 5 
 6 generic<T>
 7 structure Vector
 8 {
 9     Unit<T> x;
10     Unit<T> y;
11 }
12 
13 function Unit<int32> main()
14 {
15     variable Unit<Vector<int32>> v;
16     (v.x=10);
17     (v.y=20);
18     (result=(v.x+v.y));
19 }
20 
21 
22 /*Assembly*/
23 .data
24 .label
25      0: instruction 3
26 .code
27 // unit nativex_program_generated;
28      0: stack_reserve 0
29      1: stack_reserve 0
30      2: ret 0
31 // function Unit<int32> main()
32      3: stack_reserve 8
33 // (v.x=10);
34      4: push s8 10
35      5: convert s32 s8
36      6: stack_offset -8
37      7: push s32 0
38      8: add s32
39      9: write s32
40 // (v.y=20);
41     10: push s8 20
42     11: convert s32 s8
43     12: stack_offset -8
44     13: push s32 4
45     14: add s32
46     15: write s32
47 // (result=(v.x+v.y));
48     16: stack_offset -8
49     17: push s32 4
50     18: add s32
51     19: read s32
52     20: stack_offset -8
53     21: push s32 0
54     22: add s32
55     23: read s32
56     24: add s32
57     25: resptr
58     26: write s32
59 // function Unit<int32> main()
60     27: stack_reserve -8
61     28: ret 0
62 

    在这里可以看出实际上编译完了之后,指令集里面根本不会包含有关反省的任何信息,甚至是原先的类型也都丢掉了。当然为了解决这个问题,我给Assembly加了“资源”,那是一种通过C++的技巧封装之后,你可以不断地构造越来越大的只读数据结构,方便二进制形式的序列化和反序列化。所有的信息都存在里面,供以后使用(反正虚拟机不需要读)。

    但是泛型的全局变量、函数和契约就不一样了。泛型全局变量还是很容易做的因此我就忽略掉了。泛型的函数需要把契约的类型完整保留在指令表里面,这样在特化的时候才知道哪些地方需要被替换掉。总的来说最终的设计是这个样子的:

    首先是契约,跟上次差不多,只是命名契约被我删除了,只剩下匿名契约。总的来说我只需要在链接的时候进行检查就好了,如果发现新来的Assembly重复实现了旧Assembly已经特化过的一个契约,那就会出现链接错误。至于特化要实现在哪里,我就不在编译器上座约束了,因为这个代价更大,而且约束了灵活性。

    其次是函数。函数的泛型头现在被我修改成了:
 1 generic<T>
 2 concept Comparable
 3 {
 4     int Compare(T a, T b);
 5 }
 6 
 7 generic<T> with
 8 Comparable<T> ct
 9 function bool AreEqual(T a, T b)
10 {
11     result = ct::Compare(a, b)==0;
12 }

    你会发现最终concept变成了对一个类型或者一组类型附加的属性。泛型的函数除了这些属性以外,就只能用一些基本的东西了(当然如果你把一个变量T的地址拿出来,强转……)。这些时候所有泛型参数类型的参数、变量和结构体的地址都变成了一个表达式,譬如说&a == stack_offset+sizeof(int)*4而&b == stack_offset+sizeof(int)*4+sizeof(T)等等。而且如果AreEqual要调用其它关于T的泛型函数的话,如果其他的泛型函数对concept的要求比Comparable更多,那么就变成了编译错误。当然最简单的解决办法就是在AreEqual函数上把所有用到的concept全部加满。

    当然,最后一个泛型函数还是可以被编译成指令表和一组待计算向量的,只是链接的时候,会查看新来的Assembly需要多少还没特化的函数,然后一一为他们生成。于是现在最难的问题就变成了重构已有代码,以及如何判断concept instance是否被多个Assembly重复特化了……
posted @ 2010-06-19 00:07 陈梓瀚(vczh) 阅读(2395) | 评论 (3)编辑 收藏
    接上一篇文章。自从昨天设计了NativeX语言的泛型之后,今天又对昨天的草稿做了一下修改。设计语言的语法总是这样,首先对你自己的需求提出直接的解决方法,然后看看是不是有些新的概念跟其他概念可以合并起来变成更抽象的概念,而且又不会在实现上导致困难,也不会让编译器变的突然难写许多。经过了昨天晚上和今天早上的思考,我决定简化一下泛型的语法以及concept的内容。

    首先说语法上的。上一篇文章在定义泛型头的时候采用了generic<type T1, type T2, concept C1, concept C2>这样子的语法。本着尽量减少关键字的原则,我决定去掉type,变成generic<T1, T2, concept C1, concept C2>。原因是concept关键字还能用来定义一个契约,而type则毫无用处。而且一个契约有了concept关键字作开头,也不会跟没有type关键字的类型参数混淆。

    其次是concept。昨天定义了concept instance和concept series。其实总结到最后,concept instance无非就是concept series的一个特例。根据昨天的说法,把所有的instance都替换成series其实结果还是一样的。唯一的区别就是concept series不允许在既不是concept定义所在的Assembly也不是特化所涉及类型的Assembly里面出现它的一个特化。如果单纯去掉了concept instance的话显然会带来问题:我在AssemblyA处声明了一个concept Sortable<T>之后,没办法在AssemblyB处声明一个concept series IntSortable : Sortable<int>。因此某一些限制需要放宽一点:
    1、concept series的原始版本可以在一个既不包含concept声明和也不包含涉及的类型声明的地方声明。
    2、concept series的特化版本则必须出现在包含concept声明或者包含涉及类型声明的地方声明。

    那么其实series关键字也不需要了,因此会获得下面的写法:
 1 generic<T>
 2 concept GSortable
 3 {
 4   bool LessThan(T a, T b);
 5 }
 6 
 7 generic<T>
 8 instance Sortable : GSortable<T>
 9 {
10   LessThan = BinaryLessThen<T>;
11 }
12 
13 instance Sortable<int>
14 {
15   LessThan(a, b) = a < b;
16 }

    operation和function的区分实际上没什么大的价值,如果你真的需要一个函数指针的话,那就在参数传进去好了。而且constant也没什么必要,因为constant实际上是operation的一个特例,只是使用的时候需要多写一个口号罢了。我们会看到上面定义concept其中的操作的两个方法:指定函数和指定表达式。如果制定了表达是的话,那么该表达式将会被内联(啊)。所以constant存在的价值也就不存在了。因此我们甚至连function、operation和constant的区分也消失了,所以在语法上更加得到了简化。

    NativeX每一次引入一个新的特性的时候都是迫不得已而为之,而且一旦引入之后我总是力图将该特性设计成跟其他所有的特性正交。例如这里的泛型,所有的东西都可以加上泛型,譬如结构体、全局变量、函数、契约和契约实例。所有的东西都可以是非泛型的,也可以是泛型的。有时候我们的确需要定义一个非泛型的concept,这其实也不是什么大问题。

    不过当前的语法还会遇到C++那经典的>>问题(一直到了C++0x才正式纳入标准- -b)。这个问题有三种解决办法,第一种是不允许写成vector<vector<int> >,第二种是允许写a>>b也允许写a> >b(中间有个空格),第三种是跟VC++一样一概支持。最后一个比较困难,第二个比较奇怪,第一个比较恶习。不过结合了各种因素之后,其实我觉得支持第二个倒是最简单的办法:你仍然可以写出漂亮的代码,而且你如果自己受得了a> >b而自己恶心自己的话,那也是你自己的事……

    至于其它问题,NativeX没有逗号表达式,声明NativeX的变量需要加上variable关键字,声明NativeX的函数需要加上function关键字,所以全部迎刃而解。
posted @ 2010-06-13 23:50 陈梓瀚(vczh) 阅读(2478) | 评论 (2)编辑 收藏
    为了让更高级的语言可以编译到Vczh Library++ 3.0上面的NativeX语言,原生的泛型支持是必须有的。泛型不仅仅是一堆代码的填空题那么简单,因为编译之后的Assembly(dll)必须可以容纳泛型声明,而且其他的Assembly可以实例化包含在其他Assembly里面的泛型声明。这是非常麻烦的(被.net搞定了,jvm则由于种种原因搞不定,大概是因为jvm对assembly version的支持太差导致的,你知道.net 2.0的东西是不能引用4.0的dll的……)。不过先抛开这个不讲,虽然如何在Assembly里面实现泛型我已经心里有数了,但是这里还是从语义的层面上来考虑泛型的设计。

    在讨论之前还是要强调一下一个大前提:NativeX基本上就是一个语法更加容易看懂的C语言而已,功能完全是等价的。于是我要在NativeX上加泛型,其实也就是等于在C上面加泛型。我们使用泛型完成的事情可以有很多,譬如说定义泛型的结构体,定义泛型的函数,还有泛型的存储空间等等。首先让我们讨论泛型的结构体。最终的语法可能会跟现在不一样,因为NativeX的使命是作为一棵语法树出现的,所以做得太漂亮的价值其实不是很大。

    一、泛型结构体

    泛型的结构体还是比较容易理解的,举个小例子:
1 generic<type T>
2 structure Vector
3 {
4   T x;
5   T y;
6 }

    这样子我们就创建了一个泛型的结构体。任何熟悉C++或C#的人都知道这是什么意思,我就不做解释了。使用的时候跟我们的习惯是一样的:
1 Vector<double> v;
2 v.x = 1.0;
3 v.y = 2.0;

    于是我们创建了一个泛型的变量,然后修改了它的成员。

    二、泛型全局存储空间

    其实泛型的全局存储空间基本上等于编译器替你做好的一个key为类型的大字典。有些时候我们需要给类型加上一些附加的数据,而且是按需增长的。这就代表在编译的时候提供泛型全局存储空间的Assembly并不知道将来有多少个key要提供支持,所以创建它们的工作应该是虚拟机在链接一个想使用其他Assembly提供的全局空间的新Assembly的时候创建的。这里带来了一个问题是不同的Assembly使用相同的类型可以访问相同的全局存储空间,这里先不讨论具体实施的手段。

    语法上可能比较混淆:
1 generic<type T>
2 structure TypeStorage
3 {
4   wchar* name;
5   function T() builderFunction;//这是函数指针
6 }
7 
8 generic<type T>
9 TypeStorage<T> typeStorage;

    在一个变量上面加泛型可能会有点奇怪,不过这里的定义还是很明确的。typeStorage是全局变量的泛型,因此typeStorage<int>、typeStorage<double>甚至typeStorage<Vector<Vector<wchar*>>>等等(啊,>>问题)是代表不同的全局变量。不同的Assembly访问的typeStorage<int>都是相同的全局变量。

    三、泛型函数

    泛型函数我们也都很熟悉了。一个简单的例子可以是:
1 generic<type T>
2 T Copy(T* pt)
3 {
4   result = *pt;
5 }

    需要指出的是,NativeX并没有打算要支持泛型结构、全局存储和函数的特化或偏特化。因此我们会很惊讶的发现这样的话泛型函数唯一能做的就是复制东西了,因为它调用不了其他的非泛型函数(跟C++不一样,NativeX的泛型函数更接近于C#:编译的时候进行完全的语义分析)。虽然泛型函数可以调用其他的泛型函数,但是最终也只能做复制。因此我们要引入一个新的(其实是旧的)概念,才可以避免我们为了提供各种操作在泛型函数的参数上传入一大堆的函数指针:“概念”。

    四、泛型concept

    泛型结构体和全局存储仅仅用来保存数据,所以泛型concept只能在泛型函数上面使用。这个concept跟C++原本打算支持的concept还是很接近的,只是NativeX没有class,因此只好做一点小修改。

    泛型concept主要是为了指定一套操作的接口,然后让编译器可以完成比调用函数指针更加高效的代码。偷偷告诉大家,把他叫concept只是因为NativeX跟C很像,但其实这个概念是从Haskell来的……我们还是来看看concept怎么写吧。
 1 generic<type T>
 2 concept Addable
 3 {
 4   operation T add(T a, T b);
 5   operation T sub(T a, T b);
 6   constant T zero;
 7 }
 8 
 9 generic<type T>
10 concept Multible : Addable<T>
11 {
12   operation T mul(T a, T b);
13   operation T div(T a, T b);
14   constant T one;
15 }

    这里定义了加法和乘法的两个concept。我们可以看出concept是可以继承的,其实也是可以多重继承的。concept里面可以放操作(operation),也可以放常数(constant)。这里的常数跟全局存储的机制不同,全局存储可以自动为新类型产生可读写的空间,而concept的常数不仅是只读的,而且还不可自动产生空间。之前考虑到的一个问题就是,我们可能需要把外界提供的某个concept的operation的函数指针提取出来,有这种需要的operation可以把这个关键字替换成function,这样在实例化concept的时候,那个标记了function的操作就只能绑定一个函数而不是一个表达式了。我们可以尝试为int创建一个Multible的concept:
1 concept instance IntMultible : Multible<int>
2 {
3   operation T add(T a, T b) = a+b;
4   operation T sub(T a, T b) = a-b;
5   operation T mul(T a, T b) = a*b;
6   operation T div(T a, T b) = a/b;
7   constant T zero = 0;
8   constant T one = 1;
9 }

    于是我们可以写一个函数计算a+b*c:
1 generic<type T, concept Multible<T> multible>
2 function T AddMul(T a, T b, T c)
3 {
4   return multible.add(a, multible.mul(b, c));
5 }

    然后调用它:
1 int r = AddMul<int, IntMultible>(345);

    五、另一种concept instance

    虽然我们不允许泛型的结构体、全局存储和函数进行特化,但是因为特化实在是一个好东西。上面的concept instance是没有弹性的,因为你不可能通过一个concept instance拿到另外一个concept instance。考虑一下delphi的带引用计数的嵌套数组,如果我们想让delphi可以编译到NativeX上,则势必要支持那种东西。主要的困难在于delphi支持的带有引用计数的数组和字符串,因此在对array of array of string进行释放的时候,我们首先要拿到array of array of string的concept instance,其次在释放函数里面要拿到array of string的concept instance,最后还要拿到string的concept instance。这个用上面所提出来的方法是做不了的。因此我们引进了一种新的concept instance:叫concept series。这个跟haskell的东西又更接近了一步了,因为haskell的concept instance其实是匿名但是可特化的……

    于是现在让我们来实现Array和String,并写几个类型的Increase和Decrease的函数(函数体一部分会被忽略因为这里只是为了展示concept):
 1 structure String
 2 {
 3   int reference;
 4   wchar* content;
 5 }
 6 
 7 generic<type T>
 8 structure Array
 9 {
10   int reference;
11   int length;
12   T* items;
13 }

    我们从这里可以看出,string跟array的区别就是在于长度上面,string有0结尾而array只能通过记录一个长度来实现。现在我们来写一个用于构造缺省数值、增加引用计数和减少引用计数的concept series:
 1 generic<type T>
 2 concept Referable
 3 {
 4   operation T GetDefault();
 5   operation void Increase(T* t);
 6   operation void Decrease(T* t);
 7 }
 8 
 9 generic<type T>
10 concept series DelphiTypeReferable : Referable<T>
11 {
12 }

    concept series其实就是专门用来特化的concept instance。但是为了防止不同的Assembly特化出同一个concept series所带来的麻烦,我可能会规定允许特化concept series的地方,要么是在声明该concept series的Assembly,要么是声明涉及的类型的Assembly。因为我的Assembly不允许循环引用,因此对于同一个concept series C<T,U>来讲,就算T和U分别在不同的Assembly出现,那么也只能有一个有权限特化出它。下面来看特化具体要怎么做。首先我们特化一个简单的,string的DelphiTypeReferable:
1 concept series DelphiTypeReferable<String>
2 {
3   operation GetDefault = StringGetDefault;
4   operation Increase = StringIncrease;
5   operation Decrease = StringDecrease;
6 }

    StringGetDefault、StringIncrease和StringDecrease都是一些普通的函数,内容很简单,不用写出来。现在让我们来看看Array应该怎么做:
1 generic<type T>
2 concept series DelphiTypeReferable<Array<T>>
3 {
4   operation Array<T> GetDefault() = ArrayGetDefault<T>;
5   operation Increase = ArrayIncrease<T>;
6   operation Decrease = ArrayDecrease<T>;
7 }

    看起来好像没什么特别,不过只要想一想ArrayDecrease的实现就知道了,现在我们需要在ArrayDecrease里面访问到未知类型T的DelphiTypeReferable<T>这个concept instance。因为当自己要被干掉的时候,得将引用到的所有对象的引用计数都减少1:
 1 generic<type T>
 2 function void ArrayDecrease(Array<T>* array)
 3 {
 4   if(array->reference<=0)exit;
 5   if(--array->reference==0)
 6   {
 7     for int i = 0
 8       when i < array->length
 9       with i--
10       do DelphiTypeReferable<T>.Decrease(&array->items[i]);
11     free(array->items);
12     array->length=-1;
13     array->items=null;
14   }
15 }

    这样一大堆concept series的特化组合在一起就成为会根据类型的变化而采取不同行为的concept instance了。于是我们还剩下最后的一个问题,那么其他类型的DelphiTypeReferable应该怎么写呢?其实只需要玩一个小技巧就行了,不过在这里将会看到NativeX支持泛型的最后一个功能:
 1 generic<type T>
 2 concept series DelphiTypeReferable<T>
 3 {
 4   operation GetDefault = GenericGetDefault<T>;
 5   operation Increase = null;
 6   operation Decrease = null;
 7 }
 8 
 9 generic<type T>
10 T GenericGetDefault()
11 {
12 }

    返回null的operation可以赋值成null以表示不需要执行任何东西。如果你将一个有副作用的表达式传进去当参数的话,副作用会保证被执行。

    关于语义上的泛型就讲到这里了。
posted @ 2010-06-12 23:58 陈梓瀚(vczh) 阅读(2502) | 评论 (2)编辑 收藏
    Vczh Library++ 3.0终于实现跨Assembly调用函数了。其实在设计之初架构就已经允许了这个东西,只是一直都留着没做。现在先看两段代码,然后逐一解释指令的内容。

    首先是第一个Assembly(可以认为是dll,概念是一样的),实现了一个全局变量,然后有一个单参数的函数,返回参数跟全局变量的和(代码是从语法树生成出来的,主要是为了实现指令集里面的自动加注释功能):
 1 /*NativeX Code*/
 2 unit nativex_program_generated;
 3 variable int32 leftOperand = 0;
 4 
 5 function int32 add(int32 rightOperand)
 6     (result=(leftOperand+rightOperand));
 7 
 8 
 9 /*Assembly*/
10 .data
11 0x0000000000 00 00 00 
12 .label
13      0: instruction 7
14 .code
15 // unit nativex_program_generated;
16      0: stack_reserve 0
17 // variable int32 leftOperand = 0;
18      1: push s8 0
19      2: convert s32 s8
20      3: link_pushdata 0
21      4: write s32
22 // unit nativex_program_generated;
23      5: stack_reserve 0
24      6: ret 0
25 // function int32 add(int32 rightOperand)
26      7: stack_reserve 0
27 // (result=(leftOperand+rightOperand));
28      8: stack_offset 16
29      9: read s32
30     10: link_pushdata 0
31     11: read s32
32     12: add s32
33     13: resptr
34     14: write s32
35 // function int32 add(int32 rightOperand)
36     15: stack_reserve 0
37     16: ret 4
38 


    这段简单的加法代码没什么好解释的。窥孔优化还没做,因此会有一些垃圾在里面。在这里可以看到全局变量的访问跟参数访问的不同。全局变量使用link_pushdata,而参数使用stack_offset。link_开头的都是链接时指令,链接器会把这些东西给转换成真正的指令。因为在编译的时候并不知道全局空间的实际指针,因此只好链接的时候再做,这个时候全局空间已经生成出来了。最终link_pushdata会被转换成一个push ptr x指令,x是一个常数。

    下面是调用这个Assembly里面的另一个Assembly的main函数:

 1 /*NativeX Code*/
 2 unit nativex_program_generated;
 3 variable int32 adder alias programAdd.leftOperand;
 4 
 5 function int32 add(int32 offset) alias programAdd.add;
 6 
 7 function int32 main()
 8 {
 9     (adder=1);
10     (result=add(2));
11 }
12 
13 
14 /*Assembly*/
15 .data
16 .label
17      0: instruction 3
18 .code
19 // unit nativex_program_generated;
20      0: stack_reserve 0
21      1: stack_reserve 0
22      2: ret 0
23 // function int32 main()
24      3: stack_reserve 0
25 // (adder=1);
26      4: push s8 1
27      5: convert s32 s8
28      6: link_pushforeigndata 0
29      7: write s32
30 // (result=add(2));
31      8: push s8 2
32      9: convert s32 s8
33     10: resptr
34     11: link_callforeignfunc 1
35 // function int32 main()
36     12: stack_reserve 0
37     13: ret 0
38 

    这里主要是看看一个Assembly里面的代码是如何操作另外一个Assembly的东西的。首先定义链接符号,譬如说variable int32 adder alias programAdd.leftOperator。programAdd是第一个Assembly的名字(没有反应在代码里),然后leftOperator明显就是变量名了。因为Assembly的数据里面还保留了所有变量、函数、结构类型的声明的全部内容,因此不会出现“Dll地狱”。链接的时候可以比较一下被链接的符号的声明以及定义的连接符号的声明是否吻合,不吻合则代表要么Assembly版本有问题,要么Assembly就是错的,因此直接抛出异常不允许加载。

    在这个代码里面我们有两个符号:programAdd.leftOperator和programAdd.add。他们按照顺序分别被套上ID:0和1。因此在对adder,也就是programAdd.leftOperator赋值的时候,这里使用了链接时指令link_pushforeigndata 0,用来读入该变量的地址。调用add的时候,先push一个参数2,然后将存放结果的变量的指针也push进去,最后调用函数programAdd.add,也就是ID为1的符号了:link_callforeignfunc 1。

    链接器会把所有link_开头的指令全部通过已经加载的信息重新替换成运行是指令。显然link_pushforeigndata 0和link_callforeignfunc 1都是缺少加载时才有的信息才写成这样子的,最后会被翻译成push ptr x和call assembly_id instruction_address。

    既然可以调用外部Assembly的函数,那么把外部Assembly的函数的函数指针存放起来供以后调用也是完全可能的:
 1 /*NativeX Code*/
 2 unit nativex_program_generated;
 3 function int32 add(int32 a, int32 b)
 4     (result=(a+b));
 5 
 6 
 7 /*Assembly*/
 8 .data
 9 .label
10      0: instruction 3
11 .code
12 // unit nativex_program_generated;
13      0: stack_reserve 0
14      1: stack_reserve 0
15      2: ret 0
16 // function int32 add(int32 a, int32 b)
17      3: stack_reserve 0
18 // (result=(a+b));
19      4: stack_offset 20
20      5: read s32
21      6: stack_offset 16
22      7: read s32
23      8: add s32
24      9: resptr
25     10: write s32
26 // function int32 add(int32 a, int32 b)
27     11: stack_reserve 0
28     12: ret 8
29 

    这个我就不废话了,更加简单,连全局变量都没有了,就一个加法函数。接下来的main函数会把这个加法函数和自己的加法函数的函数指针存下来,然后调用:
 1 /*NativeX Code*/
 2 unit nativex_program_generated;
 3 function int32 main()
 4 {
 5     variable function int32(int32, int32) padd1 = add1;
 6     variable function int32(int32, int32) padd2 = add2;
 7     variable int32 a = padd1(12);
 8     variable int32 b = padd2(34);
 9     (result=((a*10)+b));
10 }
11 
12 function int32 add1(int32 a, int32 b) alias programAdd.add;
13 
14 function int32 add2(int32 a, int32 b)
15     (result=(a+b));
16 
17 
18 /*Assembly*/
19 .data
20 .label
21      0: instruction 3
22      1: instruction 40
23 .code
24 // unit nativex_program_generated;
25      0: stack_reserve 0
26      1: stack_reserve 0
27      2: ret 0
28 // function int32 main()
29      3: stack_reserve 16
30 // variable function int32(int32, int32) padd1 = add1;
31      4: link_pushforeignfunc 0
32      5: stack_offset -4
33      6: write u32
34 // variable function int32(int32, int32) padd2 = add2;
35      7: link_pushfunc 1
36      8: stack_offset -8
37      9: write u32
38 // variable int32 a = padd1(1, 2);
39     10: push s8 2
40     11: convert s32 s8
41     12: push s8 1
42     13: convert s32 s8
43     14: stack_offset -12
44     15: stack_offset -4
45     16: read u32
46     17: label
47     18: call_indirect
48 // variable int32 b = padd2(3, 4);
49     19: push s8 4
50     20: convert s32 s8
51     21: push s8 3
52     22: convert s32 s8
53     23: stack_offset -16
54     24: stack_offset -8
55     25: read u32
56     26: label
57     27: call_indirect
58 // (result=((a*10)+b));
59     28: stack_offset -16
60     29: read s32
61     30: push s8 10
62     31: convert s32 s8
63     32: stack_offset -12
64     33: read s32
65     34: mul s32
66     35: add s32
67     36: resptr
68     37: write s32
69 // function int32 main()
70     38: stack_reserve -16
71     39: ret 0
72 // function int32 add2(int32 a, int32 b)
73     40: stack_reserve 0
74 // (result=(a+b));
75     41: stack_offset 20
76     42: read s32
77     43: stack_offset 16
78     44: read s32
79     45: add s32
80     46: resptr
81     47: write s32
82 // function int32 add2(int32 a, int32 b)
83     48: stack_reserve 0
84     49: ret 8
85 

    哇哈哈。

    最新代码可以在这里获得。
posted @ 2010-06-11 07:13 陈梓瀚(vczh) 阅读(2512) | 评论 (3)编辑 收藏
    经过昨天的艰苦奋斗我终于在Vczh Library++ 3.0里完成这么一个功能了。假设我现在用代码组装了一个语法树:
 1     BasicProgramNode program;
 2     program.DefineStructure(L"Complex")
 3         .Member(L"r", t_int())
 4         .Member(L"i", t_int());
 5     program.DefineFunction(L"main").ReturnType(t_int()).Statement(
 6         s_var(t_type(L"Complex"), L"a")
 7         <<s_var(t_type(L"Complex"), L"b")
 8         <<s_var(t_type(L"Complex"), L"c")
 9         <<s_expr(e_name(L"a").Member(L"r").Assign(e_prim(1)))
10         <<s_expr(e_name(L"a").Member(L"i").Assign(e_prim(2)))
11         <<s_expr(e_name(L"b").Member(L"r").Assign(e_prim(3)))
12         <<s_expr(e_name(L"b").Member(L"i").Assign(e_prim(4)))
13         <<s_var(t_type(L"Complex"), L"x", e_name(L"a"))
14         <<s_var(t_type(L"Complex"), L"y")
15         <<s_expr(e_name(L"y").Assign(e_name(L"b")))
16         <<s_expr(e_name(L"c").Member(L"r").Assign(
17             e_name(L"x").Member(L"r"+ e_name(L"y").Member(L"r")
18             ))
19         <<s_expr(e_name(L"c").Member(L"i").Assign(
20             e_name(L"x").Member(L"i"+ e_name(L"y").Member(L"i")
21             ))
22         <<s_expr(e_result().Assign(
23             e_name(L"c").Member(L"r")*e_prim(100+ e_name(L"c").Member(L"i")
24             ))
25         );

    于是最近写的N个函数终于可以发挥作用了。首先我会拿这个program编译成指令集先跑一次,如果答案跟测试用例给出的一致那就继续往下走。接下来就将这个program还原成一个NativeX语言的字符串,然后调用NativeX的语法分析器再编译一次,这样每一个语法树的节点都有一个指向记号的属性了。这样语法树生成指令集的时候,每一个指令原本属于哪颗语法树也就都记录下来了。这个时候,将指令集输出成文本文件的时候,就可以根据位置信息使用NativeX的源代码打上注释,然后再跑一次。这样还可以通过丰富的测试用例来测试NativeX的语法分析器,而且还不会被语法分析器影响。因为program编译了一次,program->NativeX->newProgram又编译了一次,哇哈哈。结果如下:

    (窥孔优化在这个时候就可以大展身手了,不过我还没做……)
  1 /*NativeX Code*/
  2 unit nativex_program_generated;
  3 structure Complex
  4 {
  5     int32 r;
  6     int32 i;
  7 }
  8 
  9 function int32 main()
 10 {
 11     variable Complex a;
 12     variable Complex b;
 13     variable Complex c;
 14     (a.r=1);
 15     (a.i=2);
 16     (b.r=3);
 17     (b.i=4);
 18     variable Complex x = a;
 19     variable Complex y;
 20     (y=b);
 21     (c.r=(x.r+y.r));
 22     (c.i=(x.i+y.i));
 23     (result=((c.r*100)+c.i));
 24 }
 25 
 26 
 27 /*Assembly*/
 28 .data
 29 .label
 30      0: instruction 3
 31 .code
 32 // unit nativex_program_generated;
 33      0: stack_reserve 0
 34      1: stack_reserve 0
 35      2: ret 0
 36 // function int32 main()
 37      3: stack_reserve 40
 38 // (a.r=1);
 39      4: push s8 1
 40      5: convert s32 s8
 41      6: stack_offset -8
 42      7: push s32 0
 43      8: add s32
 44      9: write s32
 45 // (a.i=2);
 46     10: push s8 2
 47     11: convert s32 s8
 48     12: stack_offset -8
 49     13: push s32 4
 50     14: add s32
 51     15: write s32
 52 // (b.r=3);
 53     16: push s8 3
 54     17: convert s32 s8
 55     18: stack_offset -16
 56     19: push s32 0
 57     20: add s32
 58     21: write s32
 59 // (b.i=4);
 60     22: push s8 4
 61     23: convert s32 s8
 62     24: stack_offset -16
 63     25: push s32 4
 64     26: add s32
 65     27: write s32
 66 // variable Complex x = a;
 67     28: stack_offset -8
 68     29: stack_offset -32
 69     30: copymem 8
 70 // (y=b);
 71     31: stack_offset -16
 72     32: stack_offset -40
 73     33: copymem 8
 74 // (c.r=(x.r+y.r));
 75     34: stack_offset -40
 76     35: push s32 0
 77     36: add s32
 78     37: read s32
 79     38: stack_offset -32
 80     39: push s32 0
 81     40: add s32
 82     41: read s32
 83     42: add s32
 84     43: stack_offset -24
 85     44: push s32 0
 86     45: add s32
 87     46: write s32
 88 // (c.i=(x.i+y.i));
 89     47: stack_offset -40
 90     48: push s32 4
 91     49: add s32
 92     50: read s32
 93     51: stack_offset -32
 94     52: push s32 4
 95     53: add s32
 96     54: read s32
 97     55: add s32
 98     56: stack_offset -24
 99     57: push s32 4
100     58: add s32
101     59: write s32
102 // (result=((c.r*100)+c.i));
103     60: stack_offset -24
104     61: push s32 4
105     62: add s32
106     63: read s32
107     64: push s8 100
108     65: convert s32 s8
109     66: stack_offset -24
110     67: push s32 0
111     68: add s32
112     69: read s32
113     70: mul s32
114     71: add s32
115     72: resptr
116     73: write s32
117 // function int32 main()
118     74: stack_reserve -40
119     75: ret 0
120 

    最新的代码可以在这里获得。
posted @ 2010-06-04 19:49 陈梓瀚(vczh) 阅读(2738) | 评论 (1)编辑 收藏
    Vczh Library++ 3.0终于开始正式进行优化的工作了。当然这里的优化指的是不更改抽象语法树的情况下尽量进行更加好的代码生成。根据经验这一个部分对于程序的影响最为突出。因为一个程序员可能因为具备了编译器的一些知识而写出更加高效的代码,但是却控制不了如何将一个具体的表达式转换成汇编指令的过程。因此这部分不得不尽我所能做到最好。

    目前的进度是将表达式的代码生成分为以下几大类:
    1、计算引用地址
    2、计算引用地址的同时不运行副作用
    3、计算结果
    4、仅运行副作用
    5、将结果保存至某某地址

    除了一些非左值的表达式不具备1和2,或者一些特别复杂的左值表达式不具备2以外,每一种表达是都具有以上5种代码生成算法。这样可以尽可能将无效代码降至最少。举个例子,假设我们为表达式e生成了一串指令is,那么语句s:e;的指令显然应该是is + {pop typeof(e)}。有可能e其实是++i;,那么按照这个方法生成的代码就会有无效代码:
    push addressof(i)
    read int
    push 1
    add
    duplicate-stack-top-item int
    push addressof(i)
    write
    pop int
    我们知道红色的那些指令是不需要的。因为在编译s的时候,我们并不需要知道++i之后的结果,仅仅是想执行这个表达式的副作用。根据类似的思路,于是就有了上面的5大分类。当然每一个分类还会有自己细微的部分。如何让代码生成模块易于维护就成为一个挑战了。代码越复杂,测试也就越复杂。现在只能通过编写足够多的测试用例来尽可能覆盖更多的代码来使代码更加稳定。在修改算法的过程中还会引入新的分支,所以测试用例并不能够总是及时的跟进。于是就有了下面这个设想。

    测试用例总是要人写的,在面对编译器这么复杂的东西的时候,还没什么有效的方法可以来自动生成测试用例。因为我们需要把代码以及生成的指令集都log到一个文件里面,这样当一个用例发生了错误的时候,我们可以更加迅速地知道究竟是哪里出了问题。但是编写测试用例的时候,为了不让语法分析部分的错误导致代码生成的测试结果出错,因此只好在调用代码生成的同时不去运行语法分析的代码。于是我们需要一个将语法树转换成字符串的过程,然后将每一个语句的字符串(可以控制一个基本语句只占用一行代码)变成注释添加到汇编指令的log部分。这样我们就可以轻松知道哪些代码是属于哪个语句的。

    目前这个部分正在开发,但已经距离胜利不远了。当这个部分完成之后,就可以添加很多新的测试用例来测试被分成5类的算法了。目前的测试用例仅能保证每一种表达式都被运行过一次,但是不能保证每一个表达式的每一个代码生成算法都被执行过。

    详尽的测试可以在早期发现最大量的bug,这样可以在后续的语言种类继续建立起来的时候可以专注于该语言种类自己的测试,而不让其基层的错误让测试变得更加麻烦。
   
posted @ 2010-05-31 08:05 陈梓瀚(vczh) 阅读(2470) | 评论 (4)编辑 收藏
仅列出标题
共35页: First 10 11 12 13 14 15 16 17 18 Last