復雜的東西寫多了,如今寫點簡單的好了。由于功能上的需要,
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) 閱讀(10079) |
評論 (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) 閱讀(9848) |
評論 (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, 0, sizeof(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, 0, sizeof(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) 閱讀(11712) |
評論 (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) 閱讀(38099) |
評論 (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) 閱讀(2427) |
評論 (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) 閱讀(2509) |
評論 (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>(3, 4, 5);
五、另一種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) 閱讀(2532) |
評論 (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 0x00000000: 00 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(1, 2);
8 variable int32 b = padd2(3, 4);
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) 閱讀(2541) |
評論 (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) 閱讀(2765) |
評論 (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) 閱讀(2501) |
評論 (4) |
編輯 收藏