最早是寫 Quick Basic 跟 Turbo C++,之後用 Visual Studio/Dev-C++/Code::Blocks,其實一直以來都不需要自己撰寫 makefile,雖然要寫也是會寫,但是可以的話還是交給工具直接作。隨著時間過去會想要控制一些建置的細節,所以想找一套好的建置工具,功能不用超強大,但是希望簡單好用。進行了一些調查...
Make,有時候實在覺得不好用,所以才會想去找別的工具。
bjam,講實話真是難學難用,除了「可以一次編出多個binary」也沒真的感覺到他有什麼特點與眾不同。重點是文件一是不足,二是不好,這真是致命傷。
SCons,有趣,跟我心裡想的方向一致。如果建置想要良好的彈性,那 script 本身的描述力最好要足夠強大方便,Make script 完全不及格,jam language 算是堪用,但是都不及 SCons 直接使用 Python 當作 script language 來用的好。
CMake,最近真是紅耶。稍微看了一下,好似不錯,但是我的心思已經偏向 SCons 啦~
回頭想了一下,其實仿造 SCons 自己打造一個建構工具也沒很難,我不需要寫 parser 什麼鬼的東西,單純寫成一個 Pyhton package 就可以簡單使用了,老實說不只是不難,而是非常簡單。
bjam 難學到爆,但是自動徵測 header file 的能力 SCons 沒有,bjam 的文件有講實作原理,其實超簡單就是用 grep 直接抓,這樣我可以輕鬆實作出這點,可以補足 SCons 不足的地方。對於現代的 C++ 來說,generic programming 很常見,寫程式的過程很多時候都是要改動 header 而不改動 source。很明顯 bjam 這個專為 boost 設計的建置工具是考慮到這點了。
以後就用自己的建置工具好了。最簡單的開頭就是...
## yocomake.py
import os
def exe ( target, source ) :
cmd = 'g++ -o a.out ' + ' '.join(source)
print cmd
os.system(cmd)
## ymake
from yocomake import exe
exe ( 'a.out', ['test.cc'] )
耶,馬上就可以用了~事情總是簡單~以後要什麼功能再慢慢加上就好!這樣好像要打比 make 跟 bjam 還有更多字喔?管他的!爽就好!
想了一下以後可能需要做的東西,再多看了 SCons 兩眼,發現 SCons 的設計還真是符合我要的 XD 那我還是直接拿來改就好了 XD
2009年8月20日 星期四
路面電車
因為一段時間沒回家了,所以想說找個週末回家,於是這個週末就去火車站搭上火車前往中壢,本來還差一點趕不上這般電車的,死命的跑後來追上了。
電車開著開著,不知道為什麼覺得鐵軌兩邊的周圍的護欄都不見了,而且周圍的人也越來越多,電車的速度也變慢了,軌道也變了,變成路面電車的那種輕軌。我發現自己其實搭的是雙層的電車,雙層的路面電車,於是我走下去,想去一樓看看,結果一下去就看到司機,是俁之,然後一樓的乘客都是俁之的親戚啦,原來這是他們出遊的專車,我是不小心搭錯車搭到的。
然後就跟俁之開始聊天,俁之就一邊跟我聊天一邊開路面電車(好孩子不要學,請勿在電車行使中跟駕駛談話,當然,駕駛也請不要一邊駛車一邊跟大學同學聊天)。路面電車發展的非常完整,幾乎所有的大街小巷都有軌道,還好幾軌,真正實現 Door to Door 的軌道運輸,開電車就像是開汽車一樣方便。
後來開著開著就開到俁之台南家了,電車停在他家門口,說會在這邊吃完晚餐以後就要繼續出發。俁之說等等會駛往澎湖,問我要不要去,我說沒辦法,因為我本來是要回中壢的,來到台南已經是反方向了。
醒了,因為我並不想搭乘路面電車去澎湖 =,=
電車開著開著,不知道為什麼覺得鐵軌兩邊的周圍的護欄都不見了,而且周圍的人也越來越多,電車的速度也變慢了,軌道也變了,變成路面電車的那種輕軌。我發現自己其實搭的是雙層的電車,雙層的路面電車,於是我走下去,想去一樓看看,結果一下去就看到司機,是俁之,然後一樓的乘客都是俁之的親戚啦,原來這是他們出遊的專車,我是不小心搭錯車搭到的。
然後就跟俁之開始聊天,俁之就一邊跟我聊天一邊開路面電車(好孩子不要學,請勿在電車行使中跟駕駛談話,當然,駕駛也請不要一邊駛車一邊跟大學同學聊天)。路面電車發展的非常完整,幾乎所有的大街小巷都有軌道,還好幾軌,真正實現 Door to Door 的軌道運輸,開電車就像是開汽車一樣方便。
後來開著開著就開到俁之台南家了,電車停在他家門口,說會在這邊吃完晚餐以後就要繼續出發。俁之說等等會駛往澎湖,問我要不要去,我說沒辦法,因為我本來是要回中壢的,來到台南已經是反方向了。
醒了,因為我並不想搭乘路面電車去澎湖 =,=
2009年6月30日 星期二
C++ 沒有 split
C++ 沒有 split()。很多 script language 都有 split 這個好用的東西,但是 C++ 沒有,使用 boost::xpressive 我們可以用七行程式碼實作一個。
順帶提一下,這個函數不採用由外部傳入
std::vector<std::string> split ( const std::string &s ) {跟 Python 的 split 一樣,切割字元是三種空字元
using namespace boost::xpressive ;
sregex_token_iterator begin( s.begin(), s.end(), +_s, -1 ), end;
std::vector<std::string> v ;
std::copy( begin, end, std::back_inserter(v) );
return std::move(v) ;
}
split ( "A B C" ) ; // ["A", "B", "C"]
split ( "A BB\tCCC\n\nDDDD" ) ; // ["A", "BB", "CCC", "DDDD"]
' '
, '\t'
, 還有 '\n'
,而且連續的空白會被忽略。順帶提一下,這個函數不採用由外部傳入
std::vector<std::string>>
接收 tokens 的設計,而是用 std::move()
把結果傳出來,使用上更直覺簡單,感謝 C++0x 帶給 C++ 更強大的表述能力。
2009年5月26日 星期二
阿喵
上個禮拜二在版上看到有人說他家附近有隻流浪貓,在胡適國小附近,再問有沒有人要領養的,我看了一下照片很漂亮呢,於是就寫了信過去說想要養。當天晚上我看到的時候已經是快十點吧,我自己沒辦法從新竹過去,但是在中研院工作的學姐剛剛好在那個附近,於是學姐就幫我過去看了(一開始還說洗完澡不想出門的),學姐回來已經快要十二點了,說有看到貓,但是沒抓到,因為飼料已經被吃光了。
第二天,禮拜三,收到人家的回信了,打了電話聯絡,跟對方說我跟學姐學姐可能晚上要再過去,到時候也許會請對方幫忙找地點,於是下班之後就趕去台北囉。
到了南港捷運站,學姐就來接我過去胡適國小附近的空地,然後打了電話給原來po文的田同學(是可愛的大學生),田同學跟她的男朋友都有來,是男朋友先來的,因為那個時候田同學正在洗澡,哈。我們到的時候,剛好看到阿喵,正在一個阿姨家前面,哇~真的很漂亮~但是阿喵一看到陌生人馬上就跑了,之後也不太容易接近,我離他最近的時候不到一公尺。他們幫我介紹了那位阿姨,阿姨每天都會餵貓咪,那隻阿喵每天都會去阿姨家吃飯跟上廁所(這傢伙沒有貓沙不上廁所的),所以跟阿姨很熟,阿姨說可以幫我們抓,但是今天下午因為曾經想要抓,但是抓失敗了,所以目前阿喵警戒心很強,所以不是很好抓。阿姨跟我們說我們有空可多去那附近跟她玩,跟她熟了以後才有機會抱到她帶走,雖然說硬要抓也不是抓不到,但是這樣就算帶回家也不開心。這天晚上宣告失敗,阿姨說可以幫我們抓,於是我們把外出籃留給阿姨,請阿姨抓到的時候通知我們。真的很感謝阿姨。那天跟學姐吃過晚餐以後就回新竹了,到家已經十一點半。
之後過了兩天都沒有消息,我想說週末再去台北一趟,兩天都在那邊跟阿喵培養感情,不知道能不能抓到呢?禮拜六下午就出發前往台北,到的時候大概是五點,沒有看到阿喵,但是阿姨在,阿姨說阿喵要晚一點才會來吃飯,叫我們晚一點再來,大概七八點的時候吧,於是我跟學姐就先去吃飯了。吃玩飯之後,就到學姐家等時間,等著等著接到阿姨打來的電話了,說貓已經抓到了,於是我跟學姐就趕快過去了。
到的時候,阿姨拿著籃子在等我們,說阿喵一直叫一直叫,她很心疼,說自己開了罐頭給她吃,吃一半就把她抓起來,好像在騙她,覺得很內疚。其實阿姨在講到阿喵的時候,都可以感覺到她很希望阿喵是真的喜歡我們才跟我們走,而不是被抓走的,我覺得有點難過,因為自己好似是因為單純喜歡阿喵漂亮就來找她,如果這樣把阿喵帶走,阿姨以後就看不到阿喵了,是有點難過,但是就用力說服自己說阿喵當流浪貓也很可憐,希望被我領養以後會比較幸福。
阿姨的女兒說阿姨很喜歡喵,叫我們之後要記得記照片給她,這樣阿姨才知道阿喵過得好不好。我們道謝以後就讓阿喵跟阿姨說再見,阿姨說不要再見啦,很難過,我跟學姐就帶著阿喵走了。
去了位在附近的中研動物醫院,醫生是個很high的人,講什麼都又快又激動。檢查的結果是:女生,兩歲多,跳蚤跟耳疥蟲都有點嚴重,沒有晶片,沒有結紮。醫生沒有說有沒有寄生蟲,只說目前要處理的嚴重問題就是耳疥蟲跟跳蚤。我們買了蚤不到跟疥蟲的藥,醫生示範了一下怎麼幫貓咪擦藥跟清耳朵。我看了一下記得動作。吃過飯以後我買了貓沙跟一些用具就回新竹了。本來是想說如果貓沒有跳蚤或是傳染病的話,我就把貓放到中壢給媽媽照顧,因為媽媽也喜歡貓,而且中壢還有兩隻之前撿到的米克斯(哭哭哩跟勇者大人),這樣阿喵也有伴,就不會白天我去上班的時候她就只能一個人無聊在家。但是既然有跳蚤,我就只好先帶回新竹。
回到新竹已經很晚了,因為知道貓剛換新環境都會先躲起來,其實可以不用去抓,只要讓她知道貓沙跟食物跟水在哪裡就好,她覺得環境安全的時候自然就會出來。我把房間整理了一下,貓沙準備好,食物跟水也準備好。把阿喵抱出來之後我把她放到貓沙上面,然後是食物跟水前面,讓她知道這些東西在哪裡,之後她就可以隨便躲沒關係。我先把床底下都塞住,因為床底下她進去了我很難抓到,也很難看到,沒辦法掌握阿喵的狀況,最後她選定的地方是我的書桌下面,那邊很ok,我可以很輕鬆的找到她。一般會躲個幾天,很正常,我想我不用太擔心。
一直到我睡覺,她都沒有出來。我關燈以後躺在床上,沒多久就聽到她窸窸窣窣的爬到便盆上面上廁所,果然是美少女阿,再怎麼難忍受也不會隨便上廁所,可以不吃飯,但是上廁所很重要。上完廁所以後好似咬了兩口飼料,然後開始探險房間,我想看仔細她在玩什麼,但是她聽到我翻身的聲音,又龜回書桌下面了。然後我也睡了。
禮拜天一整天阿喵幾乎都在書桌下面,我也不太去打擾她,偶而會去摸摸她,我發現她對摸摸的反應非常好,雖然是害怕的躲在書桌下,但是一旦摸摸,她就會很配合的脖子伸長,還會翻身橋姿勢讓你摸,更誇張的是,摸一摸我收手,她竟然還會爬出來一點點繼續討摸摸(但是太出來就不會了)。就這樣,偶而摸一下摸一下,到了晚上摸著摸著竟然她就整個爬出來了,然後我停手之後,她就跑去吃飯喝水上廁所,然後開始探險房間,看樣子是完全不怕了。沒想到一天就是環境了呢,很開心。雖然很開心,但是也有傷腦筋的地方,因為禮拜天晚上她就想要上我的床睡覺 orz 但是她還沒洗澡,而且跳蚤也很多,雖然她是美少女,我也只好先拒絕她了,擋了幾次之後她就乖乖的在地上睡著了。
禮拜一一起床,幫他清了耳朵滴了藥(禮拜天也做了兩次,一天要兩次,我都上下班的時候弄),下班以後估算就是蚤不到投藥之後的48小時,可以洗澡了!太高興了!但是在這之前要先梳毛跟剪毛!她身上有好多好髒的東西,一定要先處理掉,於是開始了可怕的修羅地獄,把她的毛全部梳開,一點一點的梳,深的梳不開的就用剪刀修掉,下班時去買貓用品的時候,美容院的阿姨有教我怎麼幫貓修毛,用排梳梳阿梳,梳不開的地方就用排梳把毛跟皮膚隔開,這樣剪毛就不會傷到貓,果然是大大受用。但是梳很久以後,發現重大災難,那就是阿喵的毛很多其實不是打結也不是口香糖,而是鬼針草,她的尾巴至少有上百根的鬼針草的種子,那東西對貓毛的黏著力超強的,很多貓毛就會因為一根鬼針草結在一起,鬼針草真的很多,以我的技術,如果要剪毛可能就得趨近剃光那樣醜,所以我開始手動把鬼針草一根一根挑出來。梳毛,剪毛,挑鬼針草,這個動作大概用掉我兩個小時吧,人都快哭出來了,終於挑完了。
帶著他進去浴室,開始洗!沒想到洗澡還蠻乖的,會逃是會逃,但是就是慢慢的蛇開這樣,輕輕一撈就回來了,不會抓不會咬,所以很順利的洗好了。之後抱出來擦乾,擦乾也是很順利,因為手隔著布擦她的時候,她會覺得是在摸摸,所以很配合,還會翻身呢。但是毛實在又多又長又密,擦了超久才稍微有點乾。下一個步驟就是所有幫貓洗澡的修羅階段了:吹乾。我還沒見過貓不怕吹風機的。有的是聽到聲音就開始爆衝,有的是被風吹到就會貓爪全開跟你格鬥,安分一點的就是逃命逃到不爽反過來抓你咬你這樣。
我把阿喵放在腿上,一手一邊摸她讓她乖乖,另一首把吹風機打開,我在想我比她還要抖。令人意外的是她聽到聲音沒事耶!一邊摸她,一邊嘗試把吹風機從很遠的地方開始慢慢吹……動了……她翻身了……她覺得這樣很舒服 -_-" 這是貓嗎 -_-?就這樣,很順的吹貓,但是因為毛真的很多很密,所以吹了一個多小時才全乾,我真的快累攤了。但是事情還沒完,因為洗澡的時候發現阿喵其實還有一些結塊的毛,所以我再次進行梳毛的動作,這次弄完之後阿喵真的超漂亮的了,又漂亮又香,有家貓的感覺囉。可惜我沒相機,這時候突然覺得之前沒買真是錯了,過兩天回中壢去拿弟弟的相機來拍 :P
既然弄乾淨了,那第一件事情就是把這個又香又漂亮的美少女放到床上,哈哈~放上去之後她也很開心,捲在我的棉被上面,然後就開始睡覺了(打這篇文章的時候她也睡在枕頭上喔)。弄到這個時候,已經十二點了,也就是說我辛苦了四個多小時,終於打理完畢,以後可以開始順利的過貓貓生活啦~
然後是附註:
我花 150 買的高級釣竿型逗貓棒,這傢伙鳥都不鳥。然後買了貓草想要給她吃,但是她逃得比什麼都快,看樣子她很不喜歡貓草,阿喵真的是貓嗎……
然後名字「阿喵」,其實我還沒想到什麼名字,但是照顧她的阿姨都叫她「阿喵」,所以我就跟著叫她阿喵了,叫她會有反應喔,很聰明…
最後是很難過的事情,在版上看到有人回的文章,「我突然想到,有次我去中研動物醫院,有個媽媽帶了虎班金吉拉去看醫生,因為貓咪亂吃東西然後拉肚子,媽媽一直說她很討厭貓,問我說我要不要,她說她很想把貓丟掉,那隻貓當初是有人帶去美容丟在中研獸醫院就再也沒來帶回,後來醫生給她養的,但是後來她又不喜歡,是三個兒子說養寵物就要好好照顧牠,不知道是不是那位媽媽丟出來的,那時那媽媽有點誇張,一直囔囔要送人,很討厭貓,想要丟掉.......」雖然不一定是同一隻啦,但是虎斑的長毛貓也不是滿街跑,地點跟品種都這麼巧合,我想阿喵大概就是當初那隻吧,被兩任主人拋棄的可憐孩子。話說她現在很幸福的躺在地上睡覺。
第二天,禮拜三,收到人家的回信了,打了電話聯絡,跟對方說我跟學姐學姐可能晚上要再過去,到時候也許會請對方幫忙找地點,於是下班之後就趕去台北囉。
到了南港捷運站,學姐就來接我過去胡適國小附近的空地,然後打了電話給原來po文的田同學(是可愛的大學生),田同學跟她的男朋友都有來,是男朋友先來的,因為那個時候田同學正在洗澡,哈。我們到的時候,剛好看到阿喵,正在一個阿姨家前面,哇~真的很漂亮~但是阿喵一看到陌生人馬上就跑了,之後也不太容易接近,我離他最近的時候不到一公尺。他們幫我介紹了那位阿姨,阿姨每天都會餵貓咪,那隻阿喵每天都會去阿姨家吃飯跟上廁所(這傢伙沒有貓沙不上廁所的),所以跟阿姨很熟,阿姨說可以幫我們抓,但是今天下午因為曾經想要抓,但是抓失敗了,所以目前阿喵警戒心很強,所以不是很好抓。阿姨跟我們說我們有空可多去那附近跟她玩,跟她熟了以後才有機會抱到她帶走,雖然說硬要抓也不是抓不到,但是這樣就算帶回家也不開心。這天晚上宣告失敗,阿姨說可以幫我們抓,於是我們把外出籃留給阿姨,請阿姨抓到的時候通知我們。真的很感謝阿姨。那天跟學姐吃過晚餐以後就回新竹了,到家已經十一點半。
之後過了兩天都沒有消息,我想說週末再去台北一趟,兩天都在那邊跟阿喵培養感情,不知道能不能抓到呢?禮拜六下午就出發前往台北,到的時候大概是五點,沒有看到阿喵,但是阿姨在,阿姨說阿喵要晚一點才會來吃飯,叫我們晚一點再來,大概七八點的時候吧,於是我跟學姐就先去吃飯了。吃玩飯之後,就到學姐家等時間,等著等著接到阿姨打來的電話了,說貓已經抓到了,於是我跟學姐就趕快過去了。
到的時候,阿姨拿著籃子在等我們,說阿喵一直叫一直叫,她很心疼,說自己開了罐頭給她吃,吃一半就把她抓起來,好像在騙她,覺得很內疚。其實阿姨在講到阿喵的時候,都可以感覺到她很希望阿喵是真的喜歡我們才跟我們走,而不是被抓走的,我覺得有點難過,因為自己好似是因為單純喜歡阿喵漂亮就來找她,如果這樣把阿喵帶走,阿姨以後就看不到阿喵了,是有點難過,但是就用力說服自己說阿喵當流浪貓也很可憐,希望被我領養以後會比較幸福。
阿姨的女兒說阿姨很喜歡喵,叫我們之後要記得記照片給她,這樣阿姨才知道阿喵過得好不好。我們道謝以後就讓阿喵跟阿姨說再見,阿姨說不要再見啦,很難過,我跟學姐就帶著阿喵走了。
去了位在附近的中研動物醫院,醫生是個很high的人,講什麼都又快又激動。檢查的結果是:女生,兩歲多,跳蚤跟耳疥蟲都有點嚴重,沒有晶片,沒有結紮。醫生沒有說有沒有寄生蟲,只說目前要處理的嚴重問題就是耳疥蟲跟跳蚤。我們買了蚤不到跟疥蟲的藥,醫生示範了一下怎麼幫貓咪擦藥跟清耳朵。我看了一下記得動作。吃過飯以後我買了貓沙跟一些用具就回新竹了。本來是想說如果貓沒有跳蚤或是傳染病的話,我就把貓放到中壢給媽媽照顧,因為媽媽也喜歡貓,而且中壢還有兩隻之前撿到的米克斯(哭哭哩跟勇者大人),這樣阿喵也有伴,就不會白天我去上班的時候她就只能一個人無聊在家。但是既然有跳蚤,我就只好先帶回新竹。
回到新竹已經很晚了,因為知道貓剛換新環境都會先躲起來,其實可以不用去抓,只要讓她知道貓沙跟食物跟水在哪裡就好,她覺得環境安全的時候自然就會出來。我把房間整理了一下,貓沙準備好,食物跟水也準備好。把阿喵抱出來之後我把她放到貓沙上面,然後是食物跟水前面,讓她知道這些東西在哪裡,之後她就可以隨便躲沒關係。我先把床底下都塞住,因為床底下她進去了我很難抓到,也很難看到,沒辦法掌握阿喵的狀況,最後她選定的地方是我的書桌下面,那邊很ok,我可以很輕鬆的找到她。一般會躲個幾天,很正常,我想我不用太擔心。
一直到我睡覺,她都沒有出來。我關燈以後躺在床上,沒多久就聽到她窸窸窣窣的爬到便盆上面上廁所,果然是美少女阿,再怎麼難忍受也不會隨便上廁所,可以不吃飯,但是上廁所很重要。上完廁所以後好似咬了兩口飼料,然後開始探險房間,我想看仔細她在玩什麼,但是她聽到我翻身的聲音,又龜回書桌下面了。然後我也睡了。
禮拜天一整天阿喵幾乎都在書桌下面,我也不太去打擾她,偶而會去摸摸她,我發現她對摸摸的反應非常好,雖然是害怕的躲在書桌下,但是一旦摸摸,她就會很配合的脖子伸長,還會翻身橋姿勢讓你摸,更誇張的是,摸一摸我收手,她竟然還會爬出來一點點繼續討摸摸(但是太出來就不會了)。就這樣,偶而摸一下摸一下,到了晚上摸著摸著竟然她就整個爬出來了,然後我停手之後,她就跑去吃飯喝水上廁所,然後開始探險房間,看樣子是完全不怕了。沒想到一天就是環境了呢,很開心。雖然很開心,但是也有傷腦筋的地方,因為禮拜天晚上她就想要上我的床睡覺 orz 但是她還沒洗澡,而且跳蚤也很多,雖然她是美少女,我也只好先拒絕她了,擋了幾次之後她就乖乖的在地上睡著了。
禮拜一一起床,幫他清了耳朵滴了藥(禮拜天也做了兩次,一天要兩次,我都上下班的時候弄),下班以後估算就是蚤不到投藥之後的48小時,可以洗澡了!太高興了!但是在這之前要先梳毛跟剪毛!她身上有好多好髒的東西,一定要先處理掉,於是開始了可怕的修羅地獄,把她的毛全部梳開,一點一點的梳,深的梳不開的就用剪刀修掉,下班時去買貓用品的時候,美容院的阿姨有教我怎麼幫貓修毛,用排梳梳阿梳,梳不開的地方就用排梳把毛跟皮膚隔開,這樣剪毛就不會傷到貓,果然是大大受用。但是梳很久以後,發現重大災難,那就是阿喵的毛很多其實不是打結也不是口香糖,而是鬼針草,她的尾巴至少有上百根的鬼針草的種子,那東西對貓毛的黏著力超強的,很多貓毛就會因為一根鬼針草結在一起,鬼針草真的很多,以我的技術,如果要剪毛可能就得趨近剃光那樣醜,所以我開始手動把鬼針草一根一根挑出來。梳毛,剪毛,挑鬼針草,這個動作大概用掉我兩個小時吧,人都快哭出來了,終於挑完了。
帶著他進去浴室,開始洗!沒想到洗澡還蠻乖的,會逃是會逃,但是就是慢慢的蛇開這樣,輕輕一撈就回來了,不會抓不會咬,所以很順利的洗好了。之後抱出來擦乾,擦乾也是很順利,因為手隔著布擦她的時候,她會覺得是在摸摸,所以很配合,還會翻身呢。但是毛實在又多又長又密,擦了超久才稍微有點乾。下一個步驟就是所有幫貓洗澡的修羅階段了:吹乾。我還沒見過貓不怕吹風機的。有的是聽到聲音就開始爆衝,有的是被風吹到就會貓爪全開跟你格鬥,安分一點的就是逃命逃到不爽反過來抓你咬你這樣。
我把阿喵放在腿上,一手一邊摸她讓她乖乖,另一首把吹風機打開,我在想我比她還要抖。令人意外的是她聽到聲音沒事耶!一邊摸她,一邊嘗試把吹風機從很遠的地方開始慢慢吹……動了……她翻身了……她覺得這樣很舒服 -_-" 這是貓嗎 -_-?就這樣,很順的吹貓,但是因為毛真的很多很密,所以吹了一個多小時才全乾,我真的快累攤了。但是事情還沒完,因為洗澡的時候發現阿喵其實還有一些結塊的毛,所以我再次進行梳毛的動作,這次弄完之後阿喵真的超漂亮的了,又漂亮又香,有家貓的感覺囉。可惜我沒相機,這時候突然覺得之前沒買真是錯了,過兩天回中壢去拿弟弟的相機來拍 :P
既然弄乾淨了,那第一件事情就是把這個又香又漂亮的美少女放到床上,哈哈~放上去之後她也很開心,捲在我的棉被上面,然後就開始睡覺了(打這篇文章的時候她也睡在枕頭上喔)。弄到這個時候,已經十二點了,也就是說我辛苦了四個多小時,終於打理完畢,以後可以開始順利的過貓貓生活啦~
然後是附註:
我花 150 買的高級釣竿型逗貓棒,這傢伙鳥都不鳥。然後買了貓草想要給她吃,但是她逃得比什麼都快,看樣子她很不喜歡貓草,阿喵真的是貓嗎……
然後名字「阿喵」,其實我還沒想到什麼名字,但是照顧她的阿姨都叫她「阿喵」,所以我就跟著叫她阿喵了,叫她會有反應喔,很聰明…
最後是很難過的事情,在版上看到有人回的文章,「我突然想到,有次我去中研動物醫院,有個媽媽帶了虎班金吉拉去看醫生,因為貓咪亂吃東西然後拉肚子,媽媽一直說她很討厭貓,問我說我要不要,她說她很想把貓丟掉,那隻貓當初是有人帶去美容丟在中研獸醫院就再也沒來帶回,後來醫生給她養的,但是後來她又不喜歡,是三個兒子說養寵物就要好好照顧牠,不知道是不是那位媽媽丟出來的,那時那媽媽有點誇張,一直囔囔要送人,很討厭貓,想要丟掉.......」雖然不一定是同一隻啦,但是虎斑的長毛貓也不是滿街跑,地點跟品種都這麼巧合,我想阿喵大概就是當初那隻吧,被兩任主人拋棄的可憐孩子。話說她現在很幸福的躺在地上睡覺。
2009年5月13日 星期三
過
心中總會有幾句話在迴盪著「一流的程式設計師就是什麼都要會」、「事情總是簡單的」,雖然自己知道不一定是這樣,但是會希望每次我講這些玩笑話的時候,都會體醒自己能作到這樣的程度。還有一句常常想到的是「能把事情完成的,才是高手」這句不是玩笑話,所以也總是想著要把事情完成。
為了能成為「一流的程式設計師」,努力的督促自己在程式的每個領域都要接觸,就算不能精熟,也希望當自己遇到問題的時候,能夠知道要往哪邊找答案。為了什麼都要會,去學了多執行序,為了什麼都要會,去學了圖形化使用者介面,為了什麼都要會,去學了網路程式設計,為了什麼都要會,去學了記憶體管理,為了什麼都要會,去學了 3D 程式設計,為了什麼都要會,學了很多演算法,學了很多函式庫,學了很多語言,學了很多設計範式。
為了什麼都要會,學了 unit test,為了要用 unit test,就得會 dependence injection,為了作到 dependent injection,於是要使用 abstract factory,為了方便作到 abstract factory,所以要使用 Loki,為了要弄懂 Loki,所以要弄測底弄清楚 abstract factory design pattern,為了要弄清楚 abstract factory,所以要連帶把 factory 也弄懂。看著看著,又順便把 Design Pattern 複習了一下,然後也再努力看懂了一些 Modern C++ Design。
就是這樣,就一直學一直學,然後呢?然後呢?我就這樣一直學著程式設計,然後呢?雖然不是頂尖高手,但是也能把程式寫的還不錯,可是…我還能幹嘛呢?除了程式我沒別的東西了,把程式從我的成份裡面抽走以後,我什麼也沒有剩下了,我還剩下什麼阿?還剩下什麼阿?沒剩下什麼了,沒有了,可是我不想死耶,我好希望我會的東西不要浪費掉喔,真希望能把我會的東西都傳給別人喔,很希望自己能…可是我什麼都…我…我…我只是希望能作點什麼…什麼都好…希望能作點對這個世界大家都好的事情……
為了能成為「一流的程式設計師」,努力的督促自己在程式的每個領域都要接觸,就算不能精熟,也希望當自己遇到問題的時候,能夠知道要往哪邊找答案。為了什麼都要會,去學了多執行序,為了什麼都要會,去學了圖形化使用者介面,為了什麼都要會,去學了網路程式設計,為了什麼都要會,去學了記憶體管理,為了什麼都要會,去學了 3D 程式設計,為了什麼都要會,學了很多演算法,學了很多函式庫,學了很多語言,學了很多設計範式。
為了什麼都要會,學了 unit test,為了要用 unit test,就得會 dependence injection,為了作到 dependent injection,於是要使用 abstract factory,為了方便作到 abstract factory,所以要使用 Loki,為了要弄懂 Loki,所以要弄測底弄清楚 abstract factory design pattern,為了要弄清楚 abstract factory,所以要連帶把 factory 也弄懂。看著看著,又順便把 Design Pattern 複習了一下,然後也再努力看懂了一些 Modern C++ Design。
就是這樣,就一直學一直學,然後呢?然後呢?我就這樣一直學著程式設計,然後呢?雖然不是頂尖高手,但是也能把程式寫的還不錯,可是…我還能幹嘛呢?除了程式我沒別的東西了,把程式從我的成份裡面抽走以後,我什麼也沒有剩下了,我還剩下什麼阿?還剩下什麼阿?沒剩下什麼了,沒有了,可是我不想死耶,我好希望我會的東西不要浪費掉喔,真希望能把我會的東西都傳給別人喔,很希望自己能…可是我什麼都…我…我…我只是希望能作點什麼…什麼都好…希望能作點對這個世界大家都好的事情……
2009年3月1日 星期日
Rvalue References: C++0x Features in VC10, Part 2
Rvalue References: C++0x Features in VC10, Part 2
Part 1 of this series covered lambdas, auto, and static_assert.
今天要講的是 rvalue references,rvalue reference 可以讓我們做到兩件事情:move 語意,還有完美轉發。move 語意還有完美轉發這兩件事常常讓很多人頭大,因為 lvalue reference 跟 rvalue reference 在這兩件事裡面有很大的差異,而只有非常少數的 C++98/03 程式設計師很熟知 lvalue reference 跟 rvalue reference 的差異。這篇文章很長,因為我會把 rvalue reference 如何運作的機制解釋得非常清楚。
免驚,rvalue reference 用起來很簡單,比聽起來簡單多了。至於要怎麼在自己的程式碼裡面實作 move 語意跟完美轉發,只要依照我後面示範的例子就可以了。學習 rvalue reference 跟 move 語意是絕對值回票價的,他可以讓你的程式效能有數量級的增進,而完美轉發讓你可以輕輕鬆鬆的寫出高度泛型的程式碼。
lvalue and rvalue in C++98/03
要了解 C++0x 的 rvalue references,你必須要先了解 C++98/03 的 lvalue 跟 rvalue。
「lvalue」跟「rvalue」這兩個術語很容易被搞混,因為他們的歷史淵源本來就很混淆。(順帶一提,這兩個術語的發音是「L values」跟「R values」,雖然他們通常都寫成一個單字。)這兩個觀念是從 C 繼承而來的,而到了 C++ 這兩個觀念被探究的更清楚。為了節省時間,我就不談他們的歷史,也不談他們為什麼叫做「lvalue」跟「rvalue」,我會直接說他們在 C++98/03 是怎麼運作的。(好吧,其實也不是什麼大秘密:「L」代表「left」而「R」代表「right」。但是現在這兩個觀念已經演化了,lvalue 跟 rvalue 這兩個名字已經不能精準的表達他們現在實際上的意義。與其幫你上一整堂的歷史課,不如直接想一想「上夸克」跟「下夸克」的差別,這樣應該就懂了。)
C++03 3.10/1 提到:「Every expression is either an lvalue or an rvalue.(每一個運算式要嘛是一個 lvalue,不然就是一個 rvalue)」這很重要,請一定要記住,lvalue 跟 rvalue 所指的對象,是運算式,而不是物件(object)。
Lvalue 指的是在單一一個運算式之後所代表的一個續存物件(persistent object,也就是非暫時物件),舉例來說,
Rvalue 則是那些當整個運算式一結束的時候就會消失的無影無蹤的暫時物件。舉例來說:
注意一下
如果你想要從另外一個直覺的角度來理解一個運算式是不是 lvalue,那你只要問自己:「我能不能對這個運算式取址(address of,
前面所講的用來解釋 lvalue 跟 rvalue 的例子,在運算子多載(operator overloading)的時候並不成立。根據 C++03 5.2.2/10「A function call is an lvalue if and only if the result type is a reference(只有當一個函數傳回一個 reference 的時候,呼叫這個函數得到的結果才會是一個 lvalue)」而運算子多載本質上也是函數。所以當我們寫
Lvalue 跟 rvalue 都有可變動的(modifiable, non-const)跟不可變動的(non-modifiable, const)兩種,舉例來說:
一個
一個
每一個 reference 都有一個名字,所以一個繫結到 rvalue 的 reference,他本身是一個 lvalue(沒錯!Lvalue!)(譯註:叫的出名字的,比方說
那你有沒有曾經對一個繫結到 rvalue 的 const reference 取址過?你有!什麼時候?就是當你撰寫一個拷貝運算子
這個時候,你可能會問說:「那請問 const rvalue 跟 non-const rvalue 有什麼不一樣?如果我宣告一個 non-const Type&,我不能把這個 Type& 繫結到任何的 non-const rvalues,如果我宣告一個 const Type&,我也一樣沒辦法透過這個 reference 去改我繫結到的對象。總之我都改不到 rvalue,那到底我要怎麼樣才有辦法改變我繫結到的 rvalue?」在 C++98/03 的標準呢,這兩者還是有一點點很細微的差異的:non-const 的 rvalue 可以呼叫 non-const 的成員函數。C++ 不想讓你不小心變更一個暫時變數的值,但是當你直接呼叫一個 non-const 成員函數的時候,語意是非常明顯的,不可能是不小心,所以 C++ 允許你這麼作。而到了 C++0x,這個問題的答案有了劇烈的轉變:你可以透過這點來做到 move 語意。
賀!現在你已經有我所謂的「lvalue/rvalue 觀」,這讓你能夠看清楚一個運算式到底是 lvalue 還是 rvalue,再加上你本來就會的「const 觀念」,你現在已經完全可以理解為什麼當一個函數是
the copying problem
C++98/03 結合了不可思議的高度抽象化以及不可思議的效能,但是還是有個問題:C++98/03 超愛複製物件的。因為 value 語意,複製出來的物件是獨立的個體,修改複製出來的物件,不會影響本來物件的值。value 語意是很棒沒錯,但是也同時帶來了很多不必要的複製成本,像是
最無謂的複製成本,就是當那些來源物件準備要被銷毀的時候。請問你會不會拷貝一份文件之後馬上把原稿銷毀?這樣不是很浪費嗎。你一開始就拿著原稿就好啦,何必多費事。下面是我從一份標準委員會文件的範例改來的,我所謂的「殺手範例」,假設你有一堆 string 像是這樣…
然後你想要像這樣把他們接起來:
這樣作效率如何?(當然單指這個特例的話,我們也不用傷腦筋,因為反正他不用百萬分之一秒就可以作完了,但是我們討論的是一個更一般性的,一個語言層面的現象。)
每次呼叫
事實上呢,因為每一個被串接出來的字串,都還再被拿去串耶,所以其實那個時間複雜度是字串長度的平方,媽阿,真浪費。這點實在讓 C++ 很尷尬。事情怎麼會搞成這樣?有沒有改善的方法?
現在問題的點是這樣,
當我們要算
技術上來說,在 C++0x,其實你每次呼叫
廣義的說,當我們有辦法偵測 non-const rvalue 的時候,我們就有辦法做到「剽竊資源」這件事。當一個物件實際上繫結到的是一個 non-const rvalue,且如果這個物件有掌握某些資源(比方說記憶體),那我們就可以直接偷過來,而不用像以前那樣要複製他們,反正他們馬上就要蒸發了。當你要從一個 rvalue 建立一個物件,或是當你要指派某個 rvalue 物件的值給另外一個物件的時候,這個偷取他們資源的行為,被歸類成「moving」,然後可以被 move 的物件就具有「move 語意」。
這個觀念在很多地方都超有用的,比方說
C++0x 的 rvalue reference 讓我們可以偵測到 non-const rvalue 並且從裡面幹東西,而這點讓我們做到 move 語意。Rvalue reference 也帶給我們「把 lvalue 當作是 non-const rvalue」的能力。接下來我們就要看看 rvalue reference 是怎麼運作的!
rvalue references: initialization
C++0x 引入了一種新的 reference,rvalue reference,語法是
(我自己習慣把
那他們之間到底差在哪裡?Rvalue reference 在初始化以及多載函數決議的時候跟 lvalue reference 有不同的行為。這兩者對於初始化的時候,繫結的對象以及多載決議的時候有不同的偏好。
這些規則看起來就像是火星文,不過其實可以從兩條很簡單的規則推導出來:
阿如果你不喜歡看文字描述,比較喜歡看編譯器的錯誤訊息的話,這邊給你一個範例:
把一個 non-const rvalue reference 繫結到一個 const rvalue 是沒問題的。因為 non-const rvalue 的重點就是可以用來修改暫時變數。
雖然 lvalue reference 跟 rvalue reference 在初始化的時候很相似(只有在上面的第 18 跟 28 行不一樣),但是這個差異的確改善了多載決議時的解析能力。
rvalue references: overload resolution
函數可以根據 const 跟 non-const lvalue 來進行多載,這你已經很熟了。在 C++0x,函數還可以根據 const 跟 non-const 的 rvalue 來進行多載。當一個函數有全部的四個多載版本,你應該可以預期到,每一個函數都根據對應的運算式被呼叫。
在實務上,你真的去多載全部四個版本其實沒多大用處。真正好玩的是只多載
為啥這招有用?理由如下:
(所謂「否決權」是說,當一個候選函數被認為被淘汰了,那他就一點機會都沒有了,根本不會被列入考慮)現在我們一條一條檢視這些規則。
這邊要注意到的重點是,當你只多載
重要備註:當一個函數要以 by value 的方法傳回值的時候,要把他宣告成
move semantics: the pattern
這邊有一個範例類別,
這邊有幾點要注意的。
現在你可能會很好奇 rvalue reference 以及 move 語意,跟編譯器自動產生(隱式宣告)的建構子還有指派運算子之間的交互作用是什麼。
基本上,自動產生建構子跟指派運算子的規則,跟 move 語意沒有交互作用,只有一個例外,就是當你宣告一個 move 建構子的時候,會如同你宣告任何建構子一樣,會遮蔽預設的建構子。
move semantics: moving from lvalues
好,如果你喜歡用 copy 指派運算子來實作 copy 建構子,那很可能現在你也會想要用 move 指派運算子來實作 move 建構子。這樣作不是不可以,但是你必須要很小心。像這樣就是一個錯誤的例子:
(編譯器在這邊使用了 RVO,沒有用上 NRVO。正如我之前所說的,有些拷貝建構子的成本可以藉由 RVO 或是 NRVO 省下來,但是沒有辦法全部省下來也是很合邏輯的。剩下的這些狀況就交給 move 建構子來處理。)
上面標注了 WRONG 的那一行,呼叫的是 copy 指派運算子!他正確的通過了編譯,也順利的執行完畢,但是他就是沒有施行 move 語意。
現在是什麼狀況?你還記得 C++98/03 裡面說過:具名的 lvalue reference 就是 lvalue(當我們寫
一個具名的 rvalue reference 是一個 lvalue,是因為你可以引用(mention)他好幾次,對他作很多不同的運算。但是如果是一個 rvalue 的話,那只有第一次的運算能碰到這個他,並且有機會從裡面偷東西,後續的運算則完全沒有機會。所謂「偷」就是說不能被發現,所以第一次偷完之後,這個東西就不能再被參用到。另外一方面呢,一個不具名的 rvalue reference 不可能被重複參用,所以他可以保持他 rvalue 的特性。
如果你真的很想用你的 move 指派運算子來實作你的 move 建構子,你需要一項特異功能:把一個 lvalue 看作是一個 rvalue。C++0x 的 <utility> 裡面的
(之後我用到
除了讓你可以用 move 指派運算子來實作 move 建構子,
move semantics: movable members
C++0x 的標準類別(像是
現在你看到啦,資料成員的 move 語意很容易做到。注意
你現在應該跟 move 語意已經熟爛了(希望不是你的腦袋炸爛了)。為了測試一下你新獲得的這個能力,你就寫一個
最後的叮嚀:只要你自己寫得類別支援拷貝語意,你都該盡量幫他加上 move 語意的建構子跟指派運算子,因為編譯器不會自動幫你作這件事。因為不是只有你平常寫的程式碼可以從 move 語意獲利,當你使用 STL 的容器跟演算法的時候,你都可以省下很多的昂貴的複製成本。
the forwarding problem
C++98/03 的 lvalue,rvalue,reference,還有 template 看起來很完美,但是當程式設計師想要寫出高度泛化的函數的時候,就發現有問題了。假設你想要寫一個究極的泛型函數
如果函數沒有參數,那這個問題不存在,但是如果是有一個參數的函數呢?讓我們試試看這樣設計
問題來了,如果參數是一個 non-const rvale,這個
好吧,那不然我們試試看這樣:
如果
現在我們可以試試看多載
可惜,這個方法在多參數的時候就吃癟了。以兩個參數的例子,你就必須要多載
在 C++98/03,轉發是很嚴重的問題,而且本質上無解(用上那些噁心的 preprocessor 技巧又讓編譯明顯變慢,而且那程式碼幾乎不是人看的)。總算,rvalue reference 優雅的解決了這個問題。
(我剛剛是先解釋多載決議跟 move 語意的觀念,然後才講範例程式。現在我要反過來,我們要先看怎麼用 rvalue reference 做到完美轉發的範例程式,然後我才會說明參數推導跟 reference collapsing 的規則,因為這樣會比較容易懂)
perfect forwarding: the pattern
完美轉發讓你可以只寫一個函數就轉發所有的引數,不管你有幾個引數,也不管這些引數是什麼型別。引數的 const/non-const 跟 lvalue/rvalue 特性都會被完整的保留,讓你的
兩行!只用了兩行就做到完美轉發!太妙了!
這個例子示範了怎麼把
跟
現在讓我們看看這被後到底是幹甚麼吃的。其實他靠的就是樣板引數推導跟 reference collapse 這兩個東西。
rvalue references: template argument deduction and reference collapsing
Rvalue reference 跟樣板是以很特別的方式互動,這邊是一個範例。
(譯註:這邊講一下「參數(parameter)」跟「引數(argument)」的差別。參數指的是你宣告函數的時候,寫在參數串列的東西,比方說
藉由顯示指定 Name 的型別來印出我們
當呼叫
C++0x 會把函數的參數跟引數都轉型,來進行比對動作。
首先會先轉換函數引數的型別。有一條特殊規則(N2798 14.8.2.1 [demp.deduct.call]/3):「when the function parameter type is of the form
然後會轉換函數參數的型別。不管是 C++98/03 還是 C++0x 都一樣,會卸除 reference(不管是 lvalue 還是 rvalue 的 reference 在 C++0x 都會被卸除掉)在這個例子呢,代表
於是
做完引數型別推導之後,會進行替換動作。每一個樣板引數
看看
好,那這兩條特殊規則對我們有什麼影響?在
perfect forwarding: how std::forward() and std::identity work
回頭再看一次
現在我們知道為什麼
幸運啦,不具名的 lvalue reference 是 lvalue,然後不具名 rvalue reference 是 rvalue。所以為了要把
當呼叫
你大概好奇如果你不小心寫了
那
如果這樣寫,那就是隱式呼叫。這樣樣板引數推導就會作用,我們已經知道引數推導會幹嘛了,當你傳進來的值是一個 lvalue 的時候,他會把
move semantics: how std::move() works
現在會了樣板引數推導的特殊規則以後,讓我們回頭看
RemoveReference 的機制跟 C++0x 的 <type_traits> 裡面的
同樣的,
這就是
the past
如果想要對 rvalue referece 了解的更多,可以看他們的提案文件。不過要提一下的是那些提案所寫的東西跟現在決定的可能已經不一樣了。Rvalue reference 已經被整合到 C++0x 標準的工作文件,但是很多提案文件也許已經過時了,或是不太對,或是沒有被採納,但是如論如何那些提案還是有很多有用的資訊。
N1377,N1385,還有 N1690 是主要的提案。N2118 含括了在提入標準前的最後版本。N1784,N1821,N2377,還有 N2439 是「Extending Move Semantics To
the future
N2812「A safety Problem With Rvalue Reference (and what to do about it)」提出了一個初始化規則的變動,可以防止 rvalue reference 被繫結到 lvalue 上面。裡面提到這項改變並不會影響 move 語意跟完美轉發,所以你現在學到的東西都還是有用(但是
Stephan T. Lavavej
Visual C++ Libraries Developer
Part 1 of this series covered lambdas, auto, and static_assert.
今天要講的是 rvalue references,rvalue reference 可以讓我們做到兩件事情:move 語意,還有完美轉發。move 語意還有完美轉發這兩件事常常讓很多人頭大,因為 lvalue reference 跟 rvalue reference 在這兩件事裡面有很大的差異,而只有非常少數的 C++98/03 程式設計師很熟知 lvalue reference 跟 rvalue reference 的差異。這篇文章很長,因為我會把 rvalue reference 如何運作的機制解釋得非常清楚。
免驚,rvalue reference 用起來很簡單,比聽起來簡單多了。至於要怎麼在自己的程式碼裡面實作 move 語意跟完美轉發,只要依照我後面示範的例子就可以了。學習 rvalue reference 跟 move 語意是絕對值回票價的,他可以讓你的程式效能有數量級的增進,而完美轉發讓你可以輕輕鬆鬆的寫出高度泛型的程式碼。
lvalue and rvalue in C++98/03
要了解 C++0x 的 rvalue references,你必須要先了解 C++98/03 的 lvalue 跟 rvalue。
「lvalue」跟「rvalue」這兩個術語很容易被搞混,因為他們的歷史淵源本來就很混淆。(順帶一提,這兩個術語的發音是「L values」跟「R values」,雖然他們通常都寫成一個單字。)這兩個觀念是從 C 繼承而來的,而到了 C++ 這兩個觀念被探究的更清楚。為了節省時間,我就不談他們的歷史,也不談他們為什麼叫做「lvalue」跟「rvalue」,我會直接說他們在 C++98/03 是怎麼運作的。(好吧,其實也不是什麼大秘密:「L」代表「left」而「R」代表「right」。但是現在這兩個觀念已經演化了,lvalue 跟 rvalue 這兩個名字已經不能精準的表達他們現在實際上的意義。與其幫你上一整堂的歷史課,不如直接想一想「上夸克」跟「下夸克」的差別,這樣應該就懂了。)
C++03 3.10/1 提到:「Every expression is either an lvalue or an rvalue.(每一個運算式要嘛是一個 lvalue,不然就是一個 rvalue)」這很重要,請一定要記住,lvalue 跟 rvalue 所指的對象,是運算式,而不是物件(object)。
Lvalue 指的是在單一一個運算式之後所代表的一個續存物件(persistent object,也就是非暫時物件),舉例來說,
obj
,*ptr
,ptr[index]
,還有 ++x
都是 lvalue。Rvalue 則是那些當整個運算式一結束的時候就會消失的無影無蹤的暫時物件。舉例來說:
1729
,x + y
,std::string("meow")
,還有 x++
都是 rvalue。注意一下
++x
跟 x++
的差別。當我們寫 int x = 0;
的時候,x
是一個 lvalue,因為他代表的是一個會持續存在的物件。運算式 ++x
也是一個 lvalue,這個運算式改變了 x
的值,並且賦予他一個新的名字「++x
」,但是實際上他依然代表 x
這個續存物件。然而,運算式 x++
卻是一個 rvalue,他把本來的續存物件複製了一份,然後改變續存物件的值,最後把複製出來的值傳回去。而這個複製出來的物件是一個暫時物件,運算式結束之後,這個物件就消失了。++x
跟 x++
都會增加 x
值,但是 ++x
傳回一個續存物件,也就是他自己,而 x++
傳回的是一個暫時的複製品。這就是為什麼 ++x
是一個 lvalue,而 x++
是一個 rvalue。一個運算式是 lvalue 還是 rvalue,跟這個運算式做了些什麼沒有關係,只跟他代表的東西有關係,端看這個運算式是一個續存物件,還是一個暫時物件。如果你想要從另外一個直覺的角度來理解一個運算式是不是 lvalue,那你只要問自己:「我能不能對這個運算式取址(address of,
&
)?」如果可以,那這個運算式就是一個 lvalue,如果不行,那就是一個 rvalue。舉例來說: &obj
,&*ptr[index
],還有 &++x
都是合法的運算式(有的例子雖然很笨,但他們還是合法的)。而 &1729
,&(x + y)
,&std::string("meow")
,還有 &x++
都是不合法的運算式。為什麼這個判斷法有效?根據 C++03 5.3.1/2 當我們對一個運算元取址的時候,這個運算元必須是一個 lvalue。那為什麼標準要這樣定?因為對一個續存物件取址很安全,沒有問題,但是對一個暫時物件取值,是非常危險的行為,因為這個暫時物件馬上就會蒸發在空氣裡面,而你拿著他的記憶體位址,你不知道你會改到什麼東西。前面所講的用來解釋 lvalue 跟 rvalue 的例子,在運算子多載(operator overloading)的時候並不成立。根據 C++03 5.2.2/10「A function call is an lvalue if and only if the result type is a reference(只有當一個函數傳回一個 reference 的時候,呼叫這個函數得到的結果才會是一個 lvalue)」而運算子多載本質上也是函數。所以當我們寫
vector<int> v(10, 1729);
的時候,v[0]
是一個 lvalue,因為 operator[]()
的傳回型態是 int&
,而且 &v[0]
是合法的運算式,也很好用。而當我們寫 string s("foo"); string t("bar");
的時候,s + t
是一個 rvalue,因為 operator+()
傳回一個 string
,當然 &(s + t)
也是不合法的。Lvalue 跟 rvalue 都有可變動的(modifiable, non-const)跟不可變動的(non-modifiable, const)兩種,舉例來說:
string one("cute"); const string two("fluffy"); string three() { return "kittens"; } const string four() { return "are an essential part of a healthy diet"; } one; // modifiable lvalue two; // const lvalue three(); // modifiable rvalue four(); // const rvalue
一個
Type&
可以繫結(bind)到一個可變動的 lvalue(譯註:意思就是說使用一個可變動的 lvalue 來初始化你的 reference。比方說 int a; int& t = a;
),你可以透過這個 reference 來讀取原有的值跟寫入新的值。但是你不能把他繫結到一個 const lvalue,因為這樣會違反常數性。你也不能把他繫結到一個 non-const rvalue,這樣很危險。當你修改暫時變數的時候,可能會引來一些度爛的隱微 bug,所以 C++ 很明智的禁止這種行為了。(我應該要提一下,VC 有一個很邪惡的擴充,讓你可以做到這件事情,但是如果你編譯的時候加上 /W4 參數,那編譯器就會警告你說你用到了這個擴充功能。一般來說會警告啦~)你也不能用 const rvalue 來初始化一個 type&
,這種行為是智障加三級。(細心的讀者應該會發現我這邊沒有提到 template 引數的推導)(譯註:/W4 是 VC 最高的警告標準,預設是 /W3)一個
const Type&
可以用來繫結到任何一種型態:可變動的 lvalue,不可變動的 lvalue,可變動的 rvalue,還有不可變動的 rvalue。(然後你就可以透過這個 reference 來觀察這些被繫結的對象)每一個 reference 都有一個名字,所以一個繫結到 rvalue 的 reference,他本身是一個 lvalue(沒錯!Lvalue!)(譯註:叫的出名字的,比方說
int& a;
,當然就不是暫時變數,而是一個具體存在的物件,那當然是 lvalue)。(那如果是一個繫結到 rvalue 的 const reference 呢,就是一個 const lvalue)這很容易讓人搞混,但是這點跟之後要講的東西有很大的關係,所以我現在必需要解釋的很清楚。假設有一個函數 void observe(const string& str)
,這個函數內部的實作,str
是一個 const lvalue,只要在這個函數結束之前,這個 str
就可以被取址,取出來的位址也可以被使用,這點就算當初呼叫這個函數的時候,我們傳給他的是一個 rvalue 也一樣。就像上面的 three()
跟 four()
一樣。當我們呼叫 observe("purr")
的時候,我們會先根據 "purr"
建立一個暫時的 string
物件,然後 str
就會繫結到這個暫時物件(是個 rvalue)。three()
跟 four()
的傳回值沒有名字,所以是 rvalue,但是在 observe()
裡面,str
這東西有個名字,當然他就是一個 lvalue,正如我前面說過的啦:「Lvalue 跟 rvalue 所指的對象,是運算式,不是物件。」因為 str
可以繫結到一個運算式的結果,也可能是一個馬上就會消失的暫時變數,所以當 observe()
結束之後,我們就不應該在任何地方保存這個暫時變數的位址。那你有沒有曾經對一個繫結到 rvalue 的 const reference 取址過?你有!什麼時候?就是當你撰寫一個拷貝運算子
Foo& operator=(const Foo& other)
而且這個拷貝運算字有作「自我賦值」檢查 if (this != &other) { copy stuff; } return *this;
然後當你拷貝一個暫時變數 Foo make_foo();
的時候,你就對一個 rvalue const reference 取值啦。這個時候,你可能會問說:「那請問 const rvalue 跟 non-const rvalue 有什麼不一樣?如果我宣告一個 non-const Type&,我不能把這個 Type& 繫結到任何的 non-const rvalues,如果我宣告一個 const Type&,我也一樣沒辦法透過這個 reference 去改我繫結到的對象。總之我都改不到 rvalue,那到底我要怎麼樣才有辦法改變我繫結到的 rvalue?」在 C++98/03 的標準呢,這兩者還是有一點點很細微的差異的:non-const 的 rvalue 可以呼叫 non-const 的成員函數。C++ 不想讓你不小心變更一個暫時變數的值,但是當你直接呼叫一個 non-const 成員函數的時候,語意是非常明顯的,不可能是不小心,所以 C++ 允許你這麼作。而到了 C++0x,這個問題的答案有了劇烈的轉變:你可以透過這點來做到 move 語意。
賀!現在你已經有我所謂的「lvalue/rvalue 觀」,這讓你能夠看清楚一個運算式到底是 lvalue 還是 rvalue,再加上你本來就會的「const 觀念」,你現在已經完全可以理解為什麼當一個函數是
void mutate(string &ref)
時,mutate(one)
是合法的。但是 mutate(two)
,mutate(three())
,mutate(four())
跟 mutate("purr")
卻是不合法的了(one
,two
,three
請參照上面程式碼的定義)。如果你是一個 C++98/03 程式設計師,你本來就可以憑你的直覺(或是你的編譯器)分辨出這些東西哪些合法哪些不合法,你的直覺警報器會喔咿喔咿的跟你說 mutate(three())
是個魚目混珠的偽物。但是現在你所擁有的「lvalue/rvalue 觀」讓你更明確理解為什麼 three()
是一個 rvalue,也知道為什麼 non-const reference 不能被繫結到 rvalue。那知道這些東西有什麼用?對那些專門鑽研程式語言規則條例的「語言律師」來說,有用,但是對普通的程式設計師沒什麼用。畢竟就算你都不知道這些細節好了,其實對你也沒多大影響。但是重點來了:相較於 C++98/03,C++0x 的 lvalue/rvalue 觀念是非常非常有用的東西。(特別是當你可以分辨一個運算式是 rvalue 還是 rvalue,是 const 還是 non-const,然後根據這個差別做不同的事情。)為了要更有效的使用 C++0x 你必須要有 lvalue/rvalue 的觀念,現在萬事具備,我們可以開工啦!the copying problem
C++98/03 結合了不可思議的高度抽象化以及不可思議的效能,但是還是有個問題:C++98/03 超愛複製物件的。因為 value 語意,複製出來的物件是獨立的個體,修改複製出來的物件,不會影響本來物件的值。value 語意是很棒沒錯,但是也同時帶來了很多不必要的複製成本,像是
vector
跟 string
這類的 heavy 物件的複製就很昂貴。(heavy 是說複製起來很昂貴,比方說一個上百萬元素的 vector
的複製成本就很貴。)Return Value Optimization (RVO) 跟 Named Return Value Optimization (NEVO) 這兩種最佳化技術,在某些狀況下可以省掉拷貝建構子,減輕這個成本,但是還是沒有根治這個問題。最無謂的複製成本,就是當那些來源物件準備要被銷毀的時候。請問你會不會拷貝一份文件之後馬上把原稿銷毀?這樣不是很浪費嗎。你一開始就拿著原稿就好啦,何必多費事。下面是我從一份標準委員會文件的範例改來的,我所謂的「殺手範例」,假設你有一堆 string 像是這樣…
string s0("my mother told me that"); string s1("cute"); string s2("fluffy"); string s3("kittens"); string s4("are an essential part of a healthy diet");
然後你想要像這樣把他們接起來:
string dest = s0 + " " + s1 + " " + s2 + " " + s3 + " " + s4;
這樣作效率如何?(當然單指這個特例的話,我們也不用傷腦筋,因為反正他不用百萬分之一秒就可以作完了,但是我們討論的是一個更一般性的,一個語言層面的現象。)
每次呼叫
operator+()
的時候,就會傳回一個暫時物件,而這邊呼叫了八次,所以有八個暫時物件。每呼叫一次,他們的建構子就會作一次動態記憶體配置,拷貝目前已經拷貝的所有的字元,然後,馬上把他們銷毀,作一次動態記憶體釋放。(如果你聽說過所謂的 Small String Optimization,VC 附的 STL 實作有用上這個技術,這東西可以省去動態記憶體配置的成本。可惜這招在這邊不適用,因為我很故意的選了一個夠長的 s0
,所以就算你用上那個技術,你還是什麼都省不到!可能你也聽過一個叫做 Copy-On-Write 的最佳化技術,算了吧,這招不是用在這的,而且現在也沒人在用這東西了,因為在 multithread 的環境下這東西就絕體絕命啦。)(譯註:Small String Optimization 是一種可以把長度小於某個值的字串直接鑲嵌在類別裡面的最佳化技術,舉個例子,假設我實作的 字串類別大小是 24 bytes,那麼當我要存進去的字串長度小於 20 bytes 的時候,其實我可以用前四個 bytes 記住字串的長度,然後剩下的 20 個 bytes 直接存放字串的內容,這樣就省下另外動態配置記憶體的成本,這就是 Small String Optimization。雖然 Small String Optimization 跟 Copy-On-Write 在這邊都沒辦法用,但是其實我們還有 expression template,STLport 的 std::string
就有實作這個技術,當你在串接字串的時候,其實他沒有真的在把他們串起來,他只是先用一個 proxy class 把你要串接的字串記住,等到你要把結果指派出去的時候,他再一次把這些東西加起來。)事實上呢,因為每一個被串接出來的字串,都還再被拿去串耶,所以其實那個時間複雜度是字串長度的平方,媽阿,真浪費。這點實在讓 C++ 很尷尬。事情怎麼會搞成這樣?有沒有改善的方法?
現在問題的點是這樣,
operator+()
接收兩個參數,一個是 const string&
,另外一個是 const string&
或是 const char*
(還有其他的多載版本,但是這邊我們沒用到),但是呢,這個 operator+()
沒辦法分辨你塞給他的是 lvalue 還是 rvalue,所以他只好先建立一個暫時物件,然後再把那個暫時物件傳回來。那為什麼這邊會跟 lvalue/rvalue 有關?當我們要算
s0 + " "
的值的時候,我們一定要建立一個暫時物件。s0
是一個 lvalue,也就是是一個續存物件。所以我們不能去修改他。(好!有人注意到了!)但是當我們要算 (s0 + " ") + s1
的時候,我們可以直接把 s1
的內容接在那個暫時物件的後面嘛,這樣我們就不用建立第二個暫時物件然後還要把第一個丟掉。move 語意的核心觀念:因為 s0 + " "
是一個 rvalue,是一個運算式的結果產出的暫時物件,除了他自己,沒有人有辦法動到他。如果我們可以偵測到一個 non-const rvalue,那我們就可以任意修改裡面的值,而且還不會有人發現。operator+()
是不該去修改他的引數沒錯,但是如果他們修改的是一個 rvalue,那,誰在乎?我們可以用這個方法,把全部的字元都串接到單一一個暫時物件上面去。這個手法完全消除了不必要的複製成本跟動態記憶體配置成本,留下的是一個線性的複雜度,讚!技術上來說,在 C++0x,其實你每次呼叫
operator+()
的時候還是會傳回一個獨立的暫時物件。只是當你建立第二個暫時物件((s0 + " ") + s1
)的時候,他會從第一個暫時物件(s0 + " "
)那邊把記憶體偷過來,然後把 s1
的內容接在那塊記憶體後面(這也可能導致一個二次時間複雜度的記憶體重置動作)。「偷」這個動作呢,由指標的操作來達成:首先我們把第一個暫時物件的內部的指標拷貝到第二個暫時物件來,然後把第一個暫時物件的內部指標清成 null。這樣當第一個暫時物件要被銷毀的時候,因為他內部的指標是個 null,所以解構子啥也不會作。廣義的說,當我們有辦法偵測 non-const rvalue 的時候,我們就有辦法做到「剽竊資源」這件事。當一個物件實際上繫結到的是一個 non-const rvalue,且如果這個物件有掌握某些資源(比方說記憶體),那我們就可以直接偷過來,而不用像以前那樣要複製他們,反正他們馬上就要蒸發了。當你要從一個 rvalue 建立一個物件,或是當你要指派某個 rvalue 物件的值給另外一個物件的時候,這個偷取他們資源的行為,被歸類成「moving」,然後可以被 move 的物件就具有「move 語意」。
這個觀念在很多地方都超有用的,比方說
vector
的重新配置。當一個 vector
需要更多空間的時候(例如 push_back()
的時候),需要重新配置記憶體,然後把 vector
裡面的每一個元素從舊的記憶體拷貝到新的記憶體上面。而這些拷貝的動作可能很貴(比方說一個 vector<string>
,每一個 string
都要複製,都需要一次動態記憶體配置)。等等!舊的 vector
裡面的元素馬上就要被銷毀了耶,所以其實我們可以把裡面的東西 move 到新的那一個去,這樣就不用複製他們了。在這種狀況下,舊的記憶體區塊裡面的每一個元素都佔據一個續存的記憶體空間,而且你用來存取他們的運算式也是一個 lvalue(比方說 old_ptr[index
]),在重新配置記憶體的過程中,我們用 non-const rvalue reference 來繫結到這些元素,假裝這些舊的記憶體區塊是 non-const rvalue 有個好處,就是我們可以把裡面的東西 move 出來,省下複製物件的成本。(當我說「我要把那些 lvalue 當作是 non-const rvalue」的時候,等同於「我知道那些東西是續存的 lvalue,但是我不在乎這些 lvalue 會變成怎樣。反正這個 lvalue 就要被銷毀了,或是等等就會被指派新的值,管他的,反正我不在乎。所以如果我能從裡面偷東西,那我幹嘛不偷。」)C++0x 的 rvalue reference 讓我們可以偵測到 non-const rvalue 並且從裡面幹東西,而這點讓我們做到 move 語意。Rvalue reference 也帶給我們「把 lvalue 當作是 non-const rvalue」的能力。接下來我們就要看看 rvalue reference 是怎麼運作的!
rvalue references: initialization
C++0x 引入了一種新的 reference,rvalue reference,語法是
Type&&
跟 const Type&&
。根據目前 C++0x 的草案 N2798 8.3.2/2:「A reference type that is declared using & is called an lvalue reference, and a reference type that is declared using && is called an rvalue reference. Lvalue references and rvalue references are distinct types. Except where explicitly noted, they are semantically equivalent and commonly referred to as references.」(用 Type&
宣告的 reference 是一個 lvalue reference,用 Type&&
宣告的 reference 是 rvalue reference。lvalue reference 跟 rvalue reference 是不同的型別。除非特別註明的時候,不然他們在語意上都一樣是 reference)這代表你可以把你在 C++98/03 對 reference 的認知直接套用到 C++0x,只需要學他們之間不同的地方。(我自己習慣把
Type&
唸作「Type ref」,Type&&
唸作「Type ref ref」。這樣就是 Type
的 lvalue reference 跟 Type
的 rvalue reference。就像是一個常數指標指到一個 int
,我們寫作「int * const
」,也可以唸成「int star const」。)那他們之間到底差在哪裡?Rvalue reference 在初始化以及多載函數決議的時候跟 lvalue reference 有不同的行為。這兩者對於初始化的時候,繫結的對象以及多載決議的時候有不同的偏好。
Type&
,我們已經知道Type&
只能繫結到 non-const lvalue,其他的一概不能。const Type&
,我們也知道const Type&
可以繫結到所有的東西。Type&&
可以繫結到 non-const lvalue 還有 non-const rvalue,但是不能繫結到 const lvalue 跟 const rvalue,因為那樣會違反常數性)const Type&&
可以繫結到所有的東西。
這些規則看起來就像是火星文,不過其實可以從兩條很簡單的規則推導出來:
- 常數性,所以限制你不能把 non-const reference 繫結到 const reference。
- 為了避免不小心改到暫時變數的值,所以限制你把 non-const lvalue reference 繫結到 non-const rvalue。
阿如果你不喜歡看文字描述,比較喜歡看編譯器的錯誤訊息的話,這邊給你一個範例:
C:\Temp>type initialization.cpp #include <string> using namespace std; string modifiable_rvalue() { return "cute"; } const string const_rvalue() { return "fluffy"; } int main() { string modifiable_lvalue("kittens"); const string const_lvalue("hungry hungry zombies"); string& a = modifiable_lvalue; // Line 16 string& b = const_lvalue; // Line 17 - ERROR string& c = modifiable_rvalue(); // Line 18 - ERROR string& d = const_rvalue(); // Line 19 - ERROR const string& e = modifiable_lvalue; // Line 21 const string& f = const_lvalue; // Line 22 const string& g = modifiable_rvalue(); // Line 23 const string& h = const_rvalue(); // Line 24 string&& i = modifiable_lvalue; // Line 26 string&& j = const_lvalue; // Line 27 - ERROR string&& k = modifiable_rvalue(); // Line 28 string&& l = const_rvalue(); // Line 29 - ERROR const string&& m = modifiable_lvalue; // Line 31 const string&& n = const_lvalue; // Line 32 const string&& o = modifiable_rvalue(); // Line 33 const string&& p = const_rvalue(); // Line 34 } C:\Temp>cl /EHsc /nologo /W4 /WX initialization.cpp initialization.cpp initialization.cpp(17) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &' Conversion loses qualifiers initialization.cpp(18) : warning C4239: nonstandard extension used : 'initializing' : conversion from 'std::string' to 'std::string &' A non-const reference may only be bound to an lvalue initialization.cpp(19) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &' Conversion loses qualifiers initialization.cpp(27) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &&' Conversion loses qualifiers initialization.cpp(29) : error C2440: 'initializing' : cannot convert from 'const std::string' to 'std::string &&' Conversion loses qualifiers
把一個 non-const rvalue reference 繫結到一個 const rvalue 是沒問題的。因為 non-const rvalue 的重點就是可以用來修改暫時變數。
雖然 lvalue reference 跟 rvalue reference 在初始化的時候很相似(只有在上面的第 18 跟 28 行不一樣),但是這個差異的確改善了多載決議時的解析能力。
rvalue references: overload resolution
函數可以根據 const 跟 non-const lvalue 來進行多載,這你已經很熟了。在 C++0x,函數還可以根據 const 跟 non-const 的 rvalue 來進行多載。當一個函數有全部的四個多載版本,你應該可以預期到,每一個函數都根據對應的運算式被呼叫。
C:\Temp>type four_overloads.cpp #include <iostream> #include <ostream> #include <string> using namespace std; void meow(string& s) { cout << "meow(string&): " << s << endl; } void meow(const string& s) { cout << "meow(const string&): " << s << endl; } void meow(string&& s) { cout << "meow(string&&): " << s << endl; } void meow(const string&& s) { cout << "meow(const string&&): " << s << endl; } string strange() { return "strange()"; } const string charm() { return "charm()"; } int main() { string up("up"); const string down("down"); meow(up); meow(down); meow(strange()); meow(charm()); } C:\Temp>cl /EHsc /nologo /W4 four_overloads.cpp four_overloads.cpp C:\Temp>four_overloads meow(string&): up meow(const string&): down meow(string&&): strange() meow(const string&&): charm()
在實務上,你真的去多載全部四個版本其實沒多大用處。真正好玩的是只多載
const Type&
跟 Type&&
這兩個版本:C:\type two_overloads.cpp #include <iostream #include <ostream> #include <string> using namespace std; void purr(const string& s) { cout << "purr(const string&): " << s << endl; } void purr(string&& s) { cout << "purr(string&&): " << s << endl; } string strange() { return "strange()"; } const string charm() { return "charm()"; } int main() { string up("up"); const string down("down"); purr(up); purr(down); purr(strange()); purr(charm()); } C:\Temp>cl /EHsc /nologo /W4 two_overloads.cpp two_overloads.cpp C:\Temp>two_overloads purr(const string&): up purr(const string&): down purr(string&&): strange() purr(const string&): charm()
為啥這招有用?理由如下:
- 初始化規則有「否決權」
- Lvalue 強烈偏好繫結到 lvalue reference,rvalue 強烈偏好繫結到 rvalue reference。
- non-const 運算式輕度的偏向 non-const reference。
(所謂「否決權」是說,當一個候選函數被認為被淘汰了,那他就一點機會都沒有了,根本不會被列入考慮)現在我們一條一條檢視這些規則。
purr(up),purr(const string&)
跟purr(string&&)
都沒有被否決。但是因為up
是一個 lvalue,而 lvalue reference 強烈的偏好 lvalue,所以purr(const string&)
贏了。purr(down)
,purr(string&&)
因為不滿足常數性被否決了,purr(const string&)
又贏了!purr(strange())
,兩個人都沒有被否決,但是strange()
是一個 rvalue,而 rvalue 強烈偏好 rvalue reference,non-const 只是輕度的偏向 non-const reference ,所以purr(string&&)
贏了。purr(charm())
,purr(string&&)
因為違反常數性被否決了,所以purr(const string&)
贏了。
這邊要注意到的重點是,當你只多載
const Type&
跟 Type&&
的時候,non-const rvalue 會繫結到 Type&&
,然後其他的都繫結到 const Type&
。就這樣,這一組多載函數就可以用來做到 move 語意。重要備註:當一個函數要以 by value 的方法傳回值的時候,要把他宣告成
Type
而不是 const Type
,因為後者這種宣告方式,會阻絕 Type&&
的繫結,造成的結果是 move 語意最佳化沒辦法施行。move semantics: the pattern
這邊有一個範例類別,
remote_integer
,內部保存一個指標,指到一個動態配置來的 int
。(這就是「遠端所有權」)對你來說,這個類別的預設建構子、單一參數建構子、拷貝建構子、指派運算子,還有解構子,應該都是非常熟悉的。在這邊我多加了一個 move 語意的拷貝建構子跟指派運算子,我把這兩個函數用 #define MOVABLE
包住,這樣我就可以示範有這兩個函數跟沒有這兩個函數有什麼差別。當然啦,真的寫程式的時候不會有人這樣作。C:\Temp>type remote.cpp #include <stddef.h> #include <iostream> #include <ostream> using namespace std; class remote_integer { public: remote_integer() { cout << "Default constructor." << endl; m_p = NULL; } explicit remote_integer(const int n) { cout << "Unary constructor." << endl; m_p = new int(n); } remote_integer(const remote_integer& other) { cout << "Copy constructor." << endl; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } #ifdef MOVABLE remote_integer(remote_integer&& other) { cout << "MOVE CONSTRUCTOR." << endl; m_p = other.m_p; other.m_p = NULL; } #endif // #ifdef MOVABLE remote_integer& operator=(const remote_integer& other) { cout << "Copy assignment operator." << endl; if (this != &other) { delete m_p; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } return *this; } #ifdef MOVABLE remote_integer& operator=(remote_integer&& other) { cout << "MOVE ASSIGNMENT OPERATOR." << endl; if (this != &other) { delete m_p; m_p = other.m_p; other.m_p = NULL; } return *this; }#endif // #ifdef MOVABLE ~remote_integer() { cout << "Destructor." << endl; delete m_p; } int get() const { return m_p ? *m_p : 0; } private: int * m_p; }; remote_integer square(const remote_integer& r) { const int i = r.get(); return remote_integer(i * i); } int main() { remote_integer a(8); cout << a.get() << endl; remote_integer b(10); cout << b.get() << endl; b = square(a); cout << b.get() << endl; } C:\Temp>cl /EHsc /nologo /W4 remote.cpp remote.cpp C:\Temp>remote Unary constructor. 8 Unary constructor. 10 Unary constructor. Copy assignment operator. Destructor. 64 Destructor. Destructor. C:\Temp>cl /EHsc /nologo /W4 /DMOVABLE remote.cpp remote.cpp C:\Temp>remote Unary constructor. 8 Unary constructor. 10 Unary constructor. MOVE ASSIGNMENT OPERATOR. Destructor. 64 Destructor. Destructor.
這邊有幾點要注意的。
- 對於建構子,我們多載了 copy 跟 move 兩種版本,對於指派運算子,我們也多載了 copy 跟 move 兩種版本。當我們多載
Type&
跟const Type&&
的時候,b = square(a);
會自動偵測出可以 move 的狀況且施行 move 語意。 - 現在我們直接從別的物件那邊偷記憶體,不用再動態配置了。當偷東西的時候,我們拷貝對方的指標,然後把對方的指標清成 null,於是當對方要被解構的時候就不會去作歸還記憶體的動作。
- copy/move 語意的建構子跟指派運算子都要作自我賦值檢查。大家都知道,簡單的內建型別,像是
int
,自我賦值(譬如x = x;
)的時候是絕對不會有問題的,所以我們自己設計的型別,在自我賦值的時候也應該要做到不能出問題。當我們自己手寫 code 的時候,幾乎不太可能發生自我賦值,但是當使用像是std::sort()
這樣的演算法函式庫的時候,卻很容易發生。在 C++0x,像std::sort()
這樣的演算法可以用 move 來取代複製,但是跟指派運算子同樣的潛在危機還是存在的。
現在你可能會很好奇 rvalue reference 以及 move 語意,跟編譯器自動產生(隱式宣告)的建構子還有指派運算子之間的交互作用是什麼。
- Move 建構子跟 move 指派運算子絕對不會自動產生。
- 任何一種使用者自訂的 copy 建構子跟 move 建構子都會遮蔽預設建構子。
- 使用者自訂的 copy 建構子會遮蔽預設拷貝建構子,但是使用者自訂的 move 建構子不會遮蔽預設的 copy 建構子。
- 使用者自訂的 copy 指派運算子會遮蔽預設的指派運算子,但是使用者自訂的 move 指派運算子不會遮蔽預設的指派運算子。
基本上,自動產生建構子跟指派運算子的規則,跟 move 語意沒有交互作用,只有一個例外,就是當你宣告一個 move 建構子的時候,會如同你宣告任何建構子一樣,會遮蔽預設的建構子。
move semantics: moving from lvalues
好,如果你喜歡用 copy 指派運算子來實作 copy 建構子,那很可能現在你也會想要用 move 指派運算子來實作 move 建構子。這樣作不是不可以,但是你必須要很小心。像這樣就是一個錯誤的例子:
C:\Temp>type unified_wrong.cpp #include <stddef.h> #include <iostream> #include <ostream> using namespace std; class remote_integer { public: remote_integer() { cout << "Default constructor." << endl; m_p = NULL; } explicit remote_integer(const int n) { cout << "Unary constructor." << endl; m_p = new int(n); } remote_integer(const remote_integer& other) { cout << "Copy constructor." << endl; m_p = NULL; *this = other; } #ifdef MOVABLE remote_integer(remote_integer&& other) { cout << "MOVE CONSTRUCTOR." << endl; m_p = NULL; *this = other; // WRONG } #endif // #ifdef MOVABLE remote_integer& operator=(const remote_integer& other) { cout << "Copy assignment operator." << endl; if (this != &other) { delete m_p; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } return *this; } #ifdef MOVABLE remote_integer& operator=(remote_integer&& other) { cout << "MOVE ASSIGNMENT OPERATOR." << endl; if (this != &other) { delete m_p; m_p = other.m_p; other.m_p = NULL; } return *this; } #endif // #ifdef MOVABLE ~remote_integer() { cout << "Destructor." << endl; delete m_p; } int get() const { return m_p ? *m_p : 0; } private: int * m_p; }; remote_integer frumple(const int n) { if (n == 1729) { return remote_integer(1729); } remote_integer ret(n * n); return ret; } int main() { remote_integer x = frumple(5); cout << x.get() << endl; remote_integer y = frumple(1729); cout << y.get() << endl; } C:\Temp>cl /EHsc /nologo /W4 /O2 unified_wrong.cpp unified_wrong.cpp C:\Temp>unified_wrong Unary constructor. Copy constructor. Copy assignment operator. Destructor. 25 Unary constructor. 1729 Destructor. Destructor. C:\Temp>cl /EHsc /nologo /W4 /O2 /DMOVABLE unified_wrong.cpp unified_wrong.cpp C:\Temp>unified_wrong Unary constructor. MOVE CONSTRUCTOR. Copy assignment operator. Destructor. 25 Unary constructor. 1729 Destructor. Destructor.
(編譯器在這邊使用了 RVO,沒有用上 NRVO。正如我之前所說的,有些拷貝建構子的成本可以藉由 RVO 或是 NRVO 省下來,但是沒有辦法全部省下來也是很合邏輯的。剩下的這些狀況就交給 move 建構子來處理。)
上面標注了 WRONG 的那一行,呼叫的是 copy 指派運算子!他正確的通過了編譯,也順利的執行完畢,但是他就是沒有施行 move 語意。
現在是什麼狀況?你還記得 C++98/03 裡面說過:具名的 lvalue reference 就是 lvalue(當我們寫
int& r = *p;
那 r
就是一個 lvalue),不具名的 lvalue reference 還是 lvalue(當 vector<int> v(10, 1729);
時 v[0
] 會傳回一個 int&
,這就是一個不具名的 lvalue reference,他雖然不具名,你還是可以對他取址)。Rvalue reference 就不一樣啦:- 具名的 rvalue reference 是 lvalue。
- 不具名的 rvalue reference 是 rvalue。
一個具名的 rvalue reference 是一個 lvalue,是因為你可以引用(mention)他好幾次,對他作很多不同的運算。但是如果是一個 rvalue 的話,那只有第一次的運算能碰到這個他,並且有機會從裡面偷東西,後續的運算則完全沒有機會。所謂「偷」就是說不能被發現,所以第一次偷完之後,這個東西就不能再被參用到。另外一方面呢,一個不具名的 rvalue reference 不可能被重複參用,所以他可以保持他 rvalue 的特性。
如果你真的很想用你的 move 指派運算子來實作你的 move 建構子,你需要一項特異功能:把一個 lvalue 看作是一個 rvalue。C++0x 的 <utility> 裡面的
std::move()
提供你這個能力。VC10 將會有這個功能(其實我自己現在用的開發版已經有了),但是不好意思現在還是 VC10 CTP,沒有。所以我會教你怎麼重頭打造一個 move()。C:\Temp>type unified_right.cpp #include <stddef.h> #include <iostream> #include <ostream> using namespace std; template <typename T> struct RemoveReference { typedef T type; }; template <typename T> struct RemoveReference<T&> { typedef T type; }; template <typename T> struct RemoveReference<T&&> { typedef T type; }; template <typename T> typename RemoveReference<T>::type&& Move(T&& t) { return t; } class remote_integer { public: remote_integer() { cout << "Default constructor." << endl; m_p = NULL; } explicit remote_integer(const int n) { cout << "Unary constructor." << endl; m_p = new int(n); } remote_integer(const remote_integer& other) { cout << "Copy constructor." << endl; m_p = NULL; *this = other; } #ifdef MOVABLE remote_integer(remote_integer&& other) { cout << "MOVE CONSTRUCTOR." << endl; m_p = NULL; *this = Move(other); // RIGHT } #endif // #ifdef MOVABLE remote_integer& operator=(const remote_integer& other) { cout << "Copy assignment operator." << endl; if (this != &other) { delete m_p; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } return *this; } #ifdef MOVABLE remote_integer& operator=(remote_integer&& other) { cout << "MOVE ASSIGNMENT OPERATOR." << endl; if (this != &other) { delete m_p; m_p = other.m_p; other.m_p = NULL; } return *this; } #endif // #ifdef MOVABLE ~remote_integer() { cout << "Destructor." << endl; delete m_p; } int get() const { return m_p ? *m_p : 0; } private: int * m_p; }; remote_integer frumple(const int n) { if (n == 1729) { return remote_integer(1729); } remote_integer ret(n * n); return ret; } int main() { remote_integer x = frumple(5); cout << x.get() << endl; remote_integer y = frumple(1729); cout << y.get() << endl; } C:\Temp>cl /EHsc /nologo /W4 /O2 /DMOVABLE unified_right.cpp unified_right.cpp C:\Temp>unified_right Unary constructor. MOVE CONSTRUCTOR. MOVE ASSIGNMENT OPERATOR. Destructor. 25 Unary constructor. 1729 Destructor. Destructor.
(之後我用到
std::move()
或是我自己的 Move()
的時候,會彼此交替使用,反正他們在實作上一模一樣。)到底 std::move()
背後是怎麼做到這件事情的?此刻我只能跟你說這是「魔法」。(後面會有一個詳盡的解釋,其實這東西不複雜,但是牽扯到樣板引數推導跟 reference collapse(譯註:一個 reference 的 reference,還是 reference。C++98/03 的時候,並不允許我們對一個 reference 作 reference)。我們在講「完美轉發」的時候,還會看到這東西)跳過玄學的部份,我也可以用實際的例子來說明。當我們有一個 string
的 lvalue,比方說上面示範多載的時候提到的「up
」,std::move(up)
會呼叫 string&& std::move(string&)
這個版本,然後傳回一個不具名的 rvalue reference,當然,是一個 rvalue。那當我們有一個 string
的 rvalue,比方說上面提到的 strange()
,std::move(strange())
會呼叫 string&& std::move(string&&)
這個版本,還是傳回一個不具名的 rvalue reference,當然,也是一個 rvalue,所以不管你塞什麼東西進去,你都會拿到一個 rvalue reference,是 rvalue。除了讓你可以用 move 指派運算子來實作 move 建構子,
std::move()
在別的地方也很有用。不管什麼時候,只要你手上有一個 lvalue,但是你知道他反正馬上就要蒙主寵招了,你都可以把你的 lvalue 運算式加上 std::move()
來啟動 move 語意。move semantics: movable members
C++0x 的標準類別(像是
vector
,string
,或是 regex
)都有 move 建構子跟 move 指派運算子。而且我們已經看到要怎麼幫我們自己設計的類別加上 move 語意(剛剛那個 remote_integer
的範例)。但是如果我們設計的類別裡面,有 move 語意的類別怎麼辦(像是 vector
,string
,或是 regex
)?編譯器不會自動幫我們產生 move 建構子跟 move 指派運算子,所以我們必需要自己來。但是我們很幸運,有了 std::move()
,這件事情變的超簡單。C:\Temp>type point.cpp #include <stddef.h> #include <iostream> #include <ostream> using namespace std; template <typename T> struct RemoveReference { typedef T type; }; template <typename T> struct RemoveReference<T&> { typedef T type; }; template <typename T> struct RemoveReference<T&&> { typedef T type; }; template <typename T> typename RemoveReference<T>::type&& Move(T&& t) { return t; } class remote_integer { public: remote_integer() { cout << "Default constructor." << endl; m_p = NULL; } explicit remote_integer(const int n) { cout << "Unary constructor." << endl; m_p = new int(n); } remote_integer(const remote_integer& other) { cout << "Copy constructor." << endl; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } remote_integer(remote_integer&& other) { cout << "MOVE CONSTRUCTOR." << endl; m_p = other.m_p; other.m_p = NULL; } remote_integer& operator=(const remote_integer& other) { cout << "Copy assignment operator." << endl; if (this != &other) { delete m_p; if (other.m_p) { m_p = new int(*other.m_p); } else { m_p = NULL; } } return *this; } remote_integer& operator=(remote_integer&& other) { cout << "MOVE ASSIGNMENT OPERATOR." << endl; if (this != &other) { delete m_p; m_p = other.m_p; other.m_p = NULL; } return *this; } ~remote_integer() { cout << "Destructor." << endl; delete m_p; } int get() const { return m_p ? *m_p : 0; } private: int * m_p; }; class remote_point { public: remote_point(const int x_arg, const int y_arg) : m_x(x_arg), m_y(y_arg) { } remote_point(remote_point&& other) : m_x(Move(other.m_x)), m_y(Move(other.m_y)) { } remote_point& operator=(remote_point&& other) { m_x = Move(other.m_x); m_y = Move(other.m_y); return *this; } int x() const { return m_x.get(); } int y() const { return m_y.get(); } private: remote_integer m_x; remote_integer m_y; }; remote_point five_by_five() { return remote_point(5, 5); } remote_point taxicab(const int n) { if (n == 0) { return remote_point(1, 1728); } remote_point ret(729, 1000); return ret; } int main() { remote_point p = taxicab(43112609); cout << "(" << p.x() << ", " << p.y() << ")" << endl; p = five_by_five(); cout << "(" << p.x() << ", " << p.y() << ")" << endl; } C:\Temp>cl /EHsc /nologo /W4 /O2 point.cpp point.cpp C:\Temp>point Unary constructor. Unary constructor. MOVE CONSTRUCTOR. MOVE CONSTRUCTOR. Destructor. Destructor. (729, 1000) Unary constructor. Unary constructor. MOVE ASSIGNMENT OPERATOR. MOVE ASSIGNMENT OPERATOR. Destructor. Destructor. (5, 5) Destructor. Destructor.
現在你看到啦,資料成員的 move 語意很容易做到。注意
remote_point
的 move 指派運算子不需要作自我賦值檢查,因為 remote_integer
已經檢查過了。也注意到 remote_point
預設的拷貝建構子,指派運算子,還有解構子都正常的運作。你現在應該跟 move 語意已經熟爛了(希望不是你的腦袋炸爛了)。為了測試一下你新獲得的這個能力,你就寫一個
remote_integer
的 operator+()
來當作回家作業吧。最後的叮嚀:只要你自己寫得類別支援拷貝語意,你都該盡量幫他加上 move 語意的建構子跟指派運算子,因為編譯器不會自動幫你作這件事。因為不是只有你平常寫的程式碼可以從 move 語意獲利,當你使用 STL 的容器跟演算法的時候,你都可以省下很多的昂貴的複製成本。
the forwarding problem
C++98/03 的 lvalue,rvalue,reference,還有 template 看起來很完美,但是當程式設計師想要寫出高度泛化的函數的時候,就發現有問題了。假設你想要寫一個究極的泛型函數
outer()
,這個函數存在的人生目標就是把他拿到的所有的參數轉發給另外一個叫做 inner()
的函數,不管這些引數的型別是什麼,數量有多少,我們都希望他能夠完美的做好這件事情。這種行為有很多例子,比方說 factory
函數 make_shard<T>(args)
,要把 args
轉給 T
的建構子,然後傳回一個 shared_ptr<T>
。(這樣我們就可以把 T
型別用來管理 reference counting 的程式碼統統集中到一個地方,而效能就跟你使用的是侵入式 reference counting 一樣好。)像 function<Ret (Args)>
這樣包裝類別,把參數轉發給自己內部儲存的 functor
,也是一個例子。在這篇文章我們只專注在 outer()
函數把參數轉給 inner()
函數的部份。至於 outer()
的傳回值那是另外一回事(有的狀況很簡單,比方說 make_shared<T>(args)
永遠就是傳回 shared_ptr<T>
,但是如果你想要將傳回值完美的泛型化,就必須要用到 C++0x 的 decltype
)。如果函數沒有參數,那這個問題不存在,但是如果是有一個參數的函數呢?讓我們試試看這樣設計
outer()
:template <typename T> void outer(T& t) { inner(t); }
問題來了,如果參數是一個 non-const rvale,這個
outer()
沒辦法被呼叫(違反常數性)。又如果 inner()
接收的是 const int&
,那 inner(5)
編譯會過,但是 outer(5)
就過不了。因為 5
是一個 int
,所以 T
會被推導為 int
,但是很可惜你沒辦法把 int&
繫結到 5
。好吧,那不然我們試試看這樣:
template <typename T> void outer(const T& t) { inner(t); }
如果
inner()
接收的是 int&
,那編譯根本不會過,因為違反常數性。現在我們可以試試看多載
outer()
的兩個版本,outer(const T& t)
跟 outer(T& t)
,這個方法的確有用。當你呼叫 outer()
的時候,就像是你直接呼叫 inner()
一樣。可惜,這個方法在多參數的時候就吃癟了。以兩個參數的例子,你就必須要多載
(T1&, T2&)
,(const T1&, T2&)
,(T1&, const T2&)
,(const T1&, const T2&)
四個版本。當你的參數一多的時候,你要多載函數數目就會以指數成長。(VC9 的 tr1::bind()
光作前五個參數就已經讓人對這個世界絕望透頂,有 63 個多載函數。但是我們不做的話,我們就很難跟使用者解釋說為什麼連 f(1729)
這樣的東西編譯都不會過。為了生出這些多載函數,我們用了非常噁心的 preprocessor 機制,噁心到你連聽都不會想聽,真的。)在 C++98/03,轉發是很嚴重的問題,而且本質上無解(用上那些噁心的 preprocessor 技巧又讓編譯明顯變慢,而且那程式碼幾乎不是人看的)。總算,rvalue reference 優雅的解決了這個問題。
(我剛剛是先解釋多載決議跟 move 語意的觀念,然後才講範例程式。現在我要反過來,我們要先看怎麼用 rvalue reference 做到完美轉發的範例程式,然後我才會說明參數推導跟 reference collapsing 的規則,因為這樣會比較容易懂)
perfect forwarding: the pattern
完美轉發讓你可以只寫一個函數就轉發所有的引數,不管你有幾個引數,也不管這些引數是什麼型別。引數的 const/non-const 跟 lvalue/rvalue 特性都會被完整的保留,讓你的
outer()
跟你的 inner()
用起一模一樣,跟 move 語意一起用的話就更棒啦。(C++0x 的「variadic template」解決了「任意型別數目」的問題,所以我們要做的事情可以推廣到任意多個型別引數。)乍看之下很神奇,實際上很簡單:C:\Temp>type perfect.cpp #include <iostream> #include <ostream> using namespace std; template <typename T> struct Identity { typedef T type; }; template <typename T> T&& Forward(typename Identity<T>::type&& t) { return t; } void inner(int&, int&) { cout << "inner(int&, int&)" << endl; } void inner(int&, const int&) { cout << "inner(int&, const int&)" << endl; } void inner(const int&, int&) { cout << "inner(const int&, int&)" << endl; } void inner(const int&, const int&) { cout << "inner(const int&, const int&)" << endl; } template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) { inner(Forward<T1>(t1), Forward<T2>(t2)); } int main() { int a = 1; const int b = 2; cout << "Directly calling inner()." << endl; inner(a, a); inner(b, b); inner(3, 3); inner(a, b); inner(b, a); inner(a, 3); inner(3, a); inner(b, 3); inner(3, b); cout << endl << "Calling outer()." << endl; outer(a, a); outer(b, b); outer(3, 3); outer(a, b); outer(b, a); outer(a, 3); outer(3, a); outer(b, 3); outer(3, b); } C:\Temp>cl /EHsc /nologo /W4 perfect.cpp perfect.cpp C:\Temp>perfect Directly calling inner(). inner(int&, int&) inner(const int&, const int&) inner(const int&, const int&) inner(int&, const int&) inner(const int&, int&) inner(int&, const int&) inner(const int&, int&) inner(const int&, const int&) inner(const int&, const int&) Calling outer(). inner(int&, int&) inner(const int&, const int&) inner(const int&, const int&) inner(int&, const int&) inner(const int&, int&) inner(int&, const int&) inner(const int&, int&) inner(const int&, const int&) inner(const int&, const int&)
兩行!只用了兩行就做到完美轉發!太妙了!
這個例子示範了怎麼把
t1
跟 t2
透明的轉發給 inner()
,inner()
可以知道 t1
跟 t2
的 lvalue/rvalue 跟 const/non-const 特性,就好像他是直接被呼叫一樣。跟
std::move()
一樣,std::identity()
跟 std::forward()
都在 C++0x 的 <utility> 裡面定義(VC10 會有,不過 CTP 沒有)。我正在示範怎麼實作這兩個東西。(一樣的,我會把 std::identity()
還有 std::forward()
跟我自己的 Identity()
還有 Forward()
混用,反正他們是一模一樣的東西。)現在讓我們看看這被後到底是幹甚麼吃的。其實他靠的就是樣板引數推導跟 reference collapse 這兩個東西。
rvalue references: template argument deduction and reference collapsing
Rvalue reference 跟樣板是以很特別的方式互動,這邊是一個範例。
C:\Temp>type collapse.cpp #include <iostream> #include <ostream> #include <string> using namespace std; template <typename T> struct Name; template <> struct Name<string> { static const char * get() { return "string"; } }; template <> struct Name<const string> { static const char * get() { return "const string"; } }; template <> struct Name<string&> { static const char * get() { return "string&"; } }; template <> struct Name<const string&> { static const char * get() { return "const string&"; } }; template <> struct Name<string&&> { static const char * get() { return "string&&"; } }; template <> struct Name<const string&&> { static const char * get() { return "const string&&"; } }; template <typename T> void quark(T&& t) { cout << "t: " << t << endl; cout << "T: " << Name<T>::get() << endl; cout << "T&&: " << Name<T&&>::get() << endl; cout << endl; } string strange() { return "strange()"; } const string charm() { return "charm()"; } int main() { string up("up"); const string down("down"); quark(up); quark(down); quark(strange()); quark(charm()); } C:\Temp>cl /EHsc /nologo /W4 collapse.cpp collapse.cpp C:\Temp>collapse t: up T: string& T&&: string& t: down T: const string& T&&: const string& t: strange() T: string T&&: string&& t: charm() T: const string T&&: const string&&
(譯註:這邊講一下「參數(parameter)」跟「引數(argument)」的差別。參數指的是你宣告函數的時候,寫在參數串列的東西,比方說
int min(int a, int b);
的 a
跟 b
就是參數。引數指的是你在呼叫這個函數的時候,實際傳給函數的那個東西,比方說 int c = min(4, 7);
的 4
跟 7
就是引數。很多人有時候會用參數來指引數,其實這篇文章的原文裡面也會這樣,我翻譯的時候也會這樣,看狀況決定使用哪一個名詞。但是接下來的部份因為要仔細的區分這兩者,原文就寫得很小心,所以我也翻得很小心,當我說「參數」的時候就是很明確的「parameter」,不會是「argument」,當我說「引數」的時候就是「argument」,那你們也要看得很小心,不然會覺得很奇怪 yoco 不知道在說三小。)藉由顯示指定 Name 的型別來印出我們
T
的型別。當呼叫
quark(up)
的時候,會進行型別引數推導。quark()
有一個型別參數,但是我們沒有顯示指定他的型別(比方像 quark<X>(up)
)。所以 up
就會被拿來和宣告 quark()
時所使用的 T&&
進行比較,以進行型別引數推導。C++0x 會把函數的參數跟引數都轉型,來進行比對動作。
首先會先轉換函數引數的型別。有一條特殊規則(N2798 14.8.2.1 [demp.deduct.call]/3):「when the function parameter type is of the form
T&&
where T
is a template parameter, and the function argument is an lvalue of type A
, the type A&
is used for template argument deduction.」(當參數型別是 T&&
,且引數是一個型別 A
的 lvalue 的時候,引數型別會推導為 A&
。)(但是這條特殊規則在引數型別是 T&
或是 const T&
的時候不會作用,這樣跟 C++98/03 的行為一樣,另外在引數型別是 const T&&
的時候也不會作用。)在 quark(up)
這個例子,我們會把 string
轉成 string&
。然後會轉換函數參數的型別。不管是 C++98/03 還是 C++0x 都一樣,會卸除 reference(不管是 lvalue 還是 rvalue 的 reference 在 C++0x 都會被卸除掉)在這個例子呢,代表
T&&
會變成 T
。於是
T
就是你傳進來的引數的型別,這就是為什麼,quark(up) 會印出「T: string&
」然後 quark(down)
會印出「T: const string&
」。up
跟 down
都是 lvalue,所以他們會啟動那個特殊規則。strange()
跟 charm()
是 rvalue,所以他們用的就是本來舊有的規則,所以 quark(strange())
印出「T: string
」然後 quark(charm())
印出「T: const string
」。做完引數型別推導之後,會進行替換動作。每一個樣板引數
T
都會被替換成他經過推導之後的型別。以 quark(strange())
來說,T
是 string
,所以 T&&
就會是 string&&
。同樣的,quark(charm())
時,T
是 const string
,所以 T&&
就會是 const string&&
。但是 up
跟 down
不是這樣,他們的情況有另外一條特殊規則來處理。看看
quark(up)
,T
是 string&
,T&&
經過替換以後就變成 string& &&
,在 C++0x 裡面,reference 的 reference 會被折疊(reference collapse),折疊的規則是:「lvalue reference 有傳染性」。X& &
,X& &&
還有 X&& &
都會變成 X&
,只有 X&& &&
會變成 X&&
。所以 string& &&
會被折疊成 string&
。所以說在 template 的世界裡面,看起來像是 rvalue reference 的東西其實不一定是 rvalue reference。Name<T&&>::get()
就是一個例子。同樣的,quark(down) 會具現化 quark<const string&>()
,因為 T&&
會被替換成 string&
。在 C++98/03,你可能已經用過常數性來遮蔽樣板參數的參數型別(比方說一個樣板函數接受一個 T&
當作參數,但是當你傳一個 const Foo
物件給他的時候,T*
就會是 const Foo*
)。在 C++0x,lvalue 的特性也會遮蔽樣板的參數型別。好,那這兩條特殊規則對我們有什麼影響?在
quark()
的參數列我們使用 T&&
,T&&
會跟 quark()
接到的引數有一模一樣的型別,包含了 lvalue/rvalue 還有 const/non-const 的所有特性都全部保留了。這就是為什麼可以用 rvalue reference 來做到完美轉發。perfect forwarding: how std::forward() and std::identity work
回頭再看一次
outer()
。template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) { inner(Forward<T1>(t1), Forward<T2>(t2)); }
現在我們知道為什麼
outer()
要收 T1&&
跟 T2&&
了,因為這樣可以保留完整的型別資訊。但是為什麼要呼叫 Forward<T1>()
跟 Forward<T2>()
?回憶一下,不管是具名的 lvalue reference 還是具名的 rvalue reference 都是 lvalue。如果 outer()
直接呼叫 inner(t1, t2)
,那 inner()
就會拿到兩個 lvalue,這樣就打斷了完美轉發。幸運啦,不具名的 lvalue reference 是 lvalue,然後不具名 rvalue reference 是 rvalue。所以為了要把
t1
跟 t2
轉發給 inner()
,我們必需要透過一個函數來幫忙保留他們的型別,但是可以去掉他們的名字。這就是 std::forward()
的功用。template <typename T> struct Identity { typedef T type; }; template <typename T> T&& Forward(typename Identity<T>::type&& t) { return t; }
當呼叫
Forward<T1>(t1)
的時候,Identify
並沒有改變 T1
(我們馬上就會看到他是幹甚麼吃的)。所以 Forward<T1>(t1)
吃了一個 T1&&
又吐了一個 T1&&
。這樣 t1
的型別就保持不變(不管他吃什麼東西都一樣,string&
,const string&
,string&&
,const string&&
都一樣),但是名字不見啦!inner()
只會看到 Forward<T1>(t1)
,而這東西跟 t1
有一模一樣的型別,不管 lvalue/rvalue,const/non-const 都會跟 outer()
當初拿到的一模一樣。完美轉發就是這樣。你大概好奇如果你不小心寫了
forward<T1&&>(t1)
會怎樣。(這個錯誤還蠻誘人的,因為 outer()
接的是 T1&& t1
。)很幸運,沒什麼不好的事情會發生。Forward<T1&&>()
會拿到一個 T1&& &&
,並且傳回一個 T1&& &&
,然後會被折疊成 T1&&
。因此,Forward<T1>(t1)
跟 Forward<T1&&>(t1)
是一模一樣,但是我們偏好前面那一個,因為程式碼比較短。那
Identity
是幹啥吃的?為什麼不能直接這樣寫?template <typename T> T&& Forward(T&& t) { // BROKEN return t; }
如果這樣寫,那就是隱式呼叫。這樣樣板引數推導就會作用,我們已經知道引數推導會幹嘛了,當你傳進來的值是一個 lvalue 的時候,他會把
T&&
改成 T&
,也就是 lvalue。但是我們一直想要解決的問題,不就是 outer()
裡面不管你收到的是 lvalue 還是 rvalue 結果都會是一個 lvalue 嗎。在上面那個錯誤實作範例,Forward<T1>(t1)
會對,但是 Forward(t1)
就錯啦,他會直接把 t1
的型別傳給 inner()
,這是一個非常誘惑人的陷阱,因為他會毫髮無傷的通過編譯,然後會帶來一連串的苦難,所以 Identity
是用來阻止樣板引數型別的自動推導。在 Identity<T>::type
裡面的那兩個冒號是一層絕緣體,樣板引數型別推導的作用沒辦法穿越他,很有經驗的程式設計師應該對這些東西很熟,因為這點不管在 C++98/03 還是 C++0x 都一樣。(不過詳細的原因又是另外一件事了。)move semantics: how std::move() works
現在會了樣板引數推導的特殊規則以後,讓我們回頭看
std::move()
:template <typename T> struct RemoveReference { typedef T type; }; template <typename T> struct RemoveReference<T&> { typedef T type; }; template <typename T> struct RemoveReference<T&&> { typedef T type; }; template <typename T> typename RemoveReference<T>::type&& Move(T&& t) { return t; }
RemoveReference 的機制跟 C++0x 的 <type_traits> 裡面的
std::remove_reference
完全一樣。舉例來說 RemoveReference<string>::type
,RemoveReference<string&>::type
跟 RemoveReference<string&&>::type
都一樣是 string
。同樣的,
Move()
跟 C++0x <utility> 的 std::move()
一模一樣。- 當呼叫
Move(string)
,string
是一個 lvalue 的時候,T
會被推導成string&
,所以Move()
會拿到string&
,經過RemoveReference
處理完會變成string
,最後會傳回一個string&&
。 - 當呼叫
Move(string)
,string
是一個 const lvalue 的時候,T
會被推導成const string&
,所以Move()
拿到string&
傳給RemoveReference
之後,會傳回一個const string&&
。 - 當呼叫
Move(string)
,string
是一個 rvalue 的時候,T
會被推導成string
,Move()
拿到一個string&&
,傳回一個string&&
。 - 當呼叫
Move(string)
,string
是一個 const rvalue 的時候,T
會被推導成const string
,Move()
拿到一個const string&&
,傳回一個const string&&
。
這就是
Move()
之所以可以保留型別跟常數性,但是可以把 lvalue 變成 rvalue 的原理。the past
如果想要對 rvalue referece 了解的更多,可以看他們的提案文件。不過要提一下的是那些提案所寫的東西跟現在決定的可能已經不一樣了。Rvalue reference 已經被整合到 C++0x 標準的工作文件,但是很多提案文件也許已經過時了,或是不太對,或是沒有被採納,但是如論如何那些提案還是有很多有用的資訊。
N1377,N1385,還有 N1690 是主要的提案。N2118 含括了在提入標準前的最後版本。N1784,N1821,N2377,還有 N2439 是「Extending Move Semantics To
*this
(把 move 語意擴充到 *this
)」的演變過程,這已經被納入 C++0x 了,但是 VC10 還沒有實作。the future
N2812「A safety Problem With Rvalue Reference (and what to do about it)」提出了一個初始化規則的變動,可以防止 rvalue reference 被繫結到 lvalue 上面。裡面提到這項改變並不會影響 move 語意跟完美轉發,所以你現在學到的東西都還是有用(但是
std::move()
跟 std::forward()
的實作會改變)。Stephan T. Lavavej
Visual C++ Libraries Developer
2009年2月24日 星期二
yard 的設計理念跟手法
因為有人問到,所以提一下 yard 背後的精神跟設計手法。
從最初開始
讓我們回想一下 recursive decent parser 最原始的模樣,假設我今天想要 parse 的文法是 (ab)*xyz[cd] 那我會怎麼寫我的 parser?大概是這樣
可以看得出來,其實這些函數的架構都非常相似,首先是保留 roll back 時需要的目前指標的位置,然後是一個一個比對吃進來的序列,最後如果失敗就進行 roll back,除了裡面用來比對所呼叫的函數不一樣,整個架構都是一樣的。
C++ template function 帶來曙光
既然這些函數架構都一樣,除了裡面呼叫的函數不一樣,那讓我們想到:C++ 的 template 剛剛好可以派上用場!我們可以寫一個 function template。
於是 abab_xyz_cd() 其實可以寫成這樣…
預設型別引數
不過這邊有兩個問題要解決,一個是小問題,另外一個比較麻煩,小的那個問題是:如果我只想傳入兩個參數怎麼辦?好在 C++ 的 template 支援了預設參數的功能!我們可以設計一個函數永遠傳回 true
然後把我們的 Seq 改寫成
事情輕輕鬆鬆就解決了!注意一下第一個參數沒有預設值,因為當你說你要一個 "Seq" 的時候,想當然你不會什麼東西都不給他就叫他去比對吧。當然如果你想要實現一個不吃東西的 rule,那是另外一回事,這邊不多說,可以自己看看 yard 的 source 是怎麼做的 :)
用 class template 封裝跟命名新規則
另外一個問題比較麻煩,讓我們回頭看一下剛剛新寫好的那個規則…
其實這東西編譯根本不會過,他看起來像是一個特化函數,但是語法也不對。而且他沒有名字,你沒辦法重複使用這個新定義出來的函數,沒有人會希望每次用到這個規則的時候都要重複打上整串東西。
聰明的人馬上就想到:我可以用 function pointer 阿!
媽阿,這鬼東西編譯會過嗎?會。不過事情沒這麼美好,當你想要
的時候你就 gg 了。編譯器會跟你靠北一個你看不懂的錯誤訊息,不用看懂沒關係,簡單的原因就是因為這時候 abab_xyz_cd 不是一個型別也不是函數,而是一個變數,函數指標不是函數,是變數,而你不能把變數塞到 template 裡面當作參數,於是你現在有辦法定義一個函數,但是你辦法復用他,等於還是沒用。
解決的方法是把這些函數 rule 用 class 包起來。
然後當你要定義新規則的時候,定一個 class,繼承那些泛型的 rule,
現在一切都解決了,這些 rule 有名字,也可以被復用!而且改寫成 class 還有另外一個好處:函數指標不能被 inline,但是 funtor 可以!雖然我們取的名字不是 operator()(),但是效果一樣,這對執行效能有很大的提昇。
最後來一點小修飾
我們很接近最後的完成品了,只剩下一點點小問題,就是我們的 p 是個全域變數,我們可以設計一個 ParseState class 把目前處理到的位置跟本文封裝起來,ParseState 的細節就不提了,我們直接看改好的 Seq 是什麼樣子……
終於大功告成啦!這個是用來 match 序列的 class template,只要明白設計理念以後,其實勝下的像是 Or, Star, Plus 或是 Repeat 都很簡單了,
阿上次不是說要講 AST?嗯……下次吧,我是個沒信用的人 XD 最近先翻 C++0x rvalue reference 的文章,我覺得這比較有趣 XD
從最初開始
讓我們回想一下 recursive decent parser 最原始的模樣,假設我今天想要 parse 的文法是 (ab)*xyz[cd] 那我會怎麼寫我的 parser?大概是這樣
// ab
bool ab() {
char* q = p ;
// 成功
if ( *p++ == 'a' && *p++ == 'b' )
return true ;
// 失敗,roll back
else {
p = q ;
return false ;
}
}
// xyz
bool xyz() {
char* q = p ;
if ( *p++ == 'x' && *p++ == 'y' && *p++== 'z' )
return true ;
else {
p = q ;
return false ;
}
}
// (ab)*xyz[cd]
bool abab_xyz_cd() {
char* q = p ;
if ( abab() && xyz() && cd() )
return true ;
else {
p = q ;
return false ;
}
}
可以看得出來,其實這些函數的架構都非常相似,首先是保留 roll back 時需要的目前指標的位置,然後是一個一個比對吃進來的序列,最後如果失敗就進行 roll back,除了裡面用來比對所呼叫的函數不一樣,整個架構都是一樣的。
C++ template function 帶來曙光
既然這些函數架構都一樣,除了裡面呼叫的函數不一樣,那讓我們想到:C++ 的 template 剛剛好可以派上用場!我們可以寫一個 function template。
template < class F0, class F1, class F2 >
bool Seq () {
char* q = p ;
if ( F0() && F1() && F2() ) {
return true ;
}
else {
p = q ;
return false ;
}
}
於是 abab_xyz_cd() 其實可以寫成這樣…
//(ab)*xyz[cd]
bool Seq < abab, xyz, cd > () ;
預設型別引數
不過這邊有兩個問題要解決,一個是小問題,另外一個比較麻煩,小的那個問題是:如果我只想傳入兩個參數怎麼辦?好在 C++ 的 template 支援了預設參數的功能!我們可以設計一個函數永遠傳回 true
bool True () { return true ; }
然後把我們的 Seq 改寫成
template < class F0, class F1=True, class F2=True >
bool Seq () {
........
}
事情輕輕鬆鬆就解決了!注意一下第一個參數沒有預設值,因為當你說你要一個 "Seq" 的時候,想當然你不會什麼東西都不給他就叫他去比對吧。當然如果你想要實現一個不吃東西的 rule,那是另外一回事,這邊不多說,可以自己看看 yard 的 source 是怎麼做的 :)
用 class template 封裝跟命名新規則
另外一個問題比較麻煩,讓我們回頭看一下剛剛新寫好的那個規則…
//(ab)*xyz[cd]
bool Seq < abab, xyz, cd > () ;
其實這東西編譯根本不會過,他看起來像是一個特化函數,但是語法也不對。而且他沒有名字,你沒辦法重複使用這個新定義出來的函數,沒有人會希望每次用到這個規則的時候都要重複打上整串東西。
聰明的人馬上就想到:我可以用 function pointer 阿!
bool (*abab_xyz_cd)() = Seq < abab, xyz, cd > ;
媽阿,這鬼東西編譯會過嗎?會。不過事情沒這麼美好,當你想要
//(ab)*xyz[cd]gg
bool (*abab_xyz_cd_gg)() = Seq < abab_xyz_cd, gg > ;
的時候你就 gg 了。編譯器會跟你靠北一個你看不懂的錯誤訊息,不用看懂沒關係,簡單的原因就是因為這時候 abab_xyz_cd 不是一個型別也不是函數,而是一個變數,函數指標不是函數,是變數,而你不能把變數塞到 template 裡面當作參數,於是你現在有辦法定義一個函數,但是你辦法復用他,等於還是沒用。
解決的方法是把這些函數 rule 用 class 包起來。
template < class F0, class F1, class F2 >
class Seq {
bool match() {
char* q = p ;
if (F0.match()&&F1.match()&&F2.match()) {
return true ;
}
else {
p = q ;
return false ;
}
}
}
然後當你要定義新規則的時候,定一個 class,繼承那些泛型的 rule,
class abab_xyz_cd
: Seq < abab, xyz, cd >
{} ;
現在一切都解決了,這些 rule 有名字,也可以被復用!而且改寫成 class 還有另外一個好處:函數指標不能被 inline,但是 funtor 可以!雖然我們取的名字不是 operator()(),但是效果一樣,這對執行效能有很大的提昇。
最後來一點小修飾
我們很接近最後的完成品了,只剩下一點點小問題,就是我們的 p 是個全域變數,我們可以設計一個 ParseState class 把目前處理到的位置跟本文封裝起來,ParseState 的細節就不提了,我們直接看改好的 Seq 是什麼樣子……
template < class F0, class F1, class F2 >
class Seq {
bool match(ParseState& p) {
ParseState::Iter q = p.pos() ;
if (F0.match(p)&&F1.match(p)&&F2.match(p)) {
return true ;
}
else {
p.setPos(q) ;
return false ;
}
}
}
終於大功告成啦!這個是用來 match 序列的 class template,只要明白設計理念以後,其實勝下的像是 Or, Star, Plus 或是 Repeat 都很簡單了,
阿上次不是說要講 AST?嗯……下次吧,我是個沒信用的人 XD 最近先翻 C++0x rvalue reference 的文章,我覺得這比較有趣 XD
yard parser framework
Yet Another Recursive Descent (YARD) parsing framework for C++
網站 http://code.google.com/p/yardparser/
網站上面沒有什麼教學,只有下載,然後抓下來的東西裡面有 demo 程式,因為本身非常簡單,所以大致上是看了就懂,但是我還是想寫一篇教學,這樣大家可以減少一點摸索的時間。
這篇文章會先介紹 yard 跟其他 parsing gramework 的比較,然後我會從非常簡單的範例開始出發,一直到最後給一個很難的範例,讓你們熟悉如何使用 yard 建立自己的 grammar。在學會了如何使用 yard 建立自己的 grammar 之後,我會示範如何使用 yard 來自動建立 abstract syntax tree (AST),並且把 yard 使用在自己的程式裡面。
跟 boost::spirit 的比較
yard 跟 boost::spirit 一樣的地方是他們的 grammar 都不限於 LR(1),可以寫 EBNF 的 parsing rule,語法好寫好讀。boost::spirit 強勢的地方在於支援 dynamic parsing,也就是 parsing rule 可以在 runtime 動態改變。
boost::spirit 最大的弱點在於編譯效能,boost::spirit 很難用來開發大型的 parser,當你的 parsing rule 到了 30~40 以後,編譯時間就快要一個小時,一個 78 parsing rule 的 grammar,編譯要兩個小時。
執行效能也是 boost::spirit 的一個問題,每個 parsing rule 被引用的時候,都涉及一次 virtual function 的呼叫。相較於此,yard 的編譯速度非常快速!編譯完整的 c 語言語法 + XML 語法 + scheme 語法,全部加起來不用一秒。
跟 yacc 的比較
沒什麼好比的 -________-|| yacc 再見再見
簡單教學範例
好,在使用 yard 的時候,我們都要 include,如果你要 parse 一般簡單的文字的話,順便 include ,然後為了方便,就 using namespace 吧!所以你開發的每個 grammar 原始碼架構大概是這樣的。
那這篇文章之後的範例就不寫這部份了,只著重在 grammar 的地方。
yard 的設計是建立在 C++ 強大的 template 機制上面,每一個 parsing rule 就是一個 struct,藉由組合不同的 struct 來完成複雜的 grammar。
先來一個最簡單的例子
這樣就完成一個可以用來 parse "AB" 這個字串的 rule 了,CharSeq 是 yard 裡面已經定義好的 struct,會從來源讀入一個一個字元來進行 match 的動作。實做細節其實很不難,但是很有創意!有興趣的去看翻一下原始碼就知道。
再來一個稍微難一點的
不用說,Star 也是 yard 裡面已經定義好的一個 struct,可以 match 零到無限多個你放在 < > 裡面當作 template 參數的規則。
馬上應用剛剛學到的兩個…
Seq 也是 yard 已經有的 struct,可以用來 match 一串序列的 rule,他跟 CharSeq 一樣都可以接不同數量的 template 參數,但是也不是沒有上限,他內部設計是 10 還是 16 我忘記了,你可以自己改,但是最後的限制會取決於你的編譯器所支援的上限。
有的時候你會想 match 多種可能的其中一種
當你用 Or 的時候,你就可以只 match 其中一種 rule,注意一下,當某個順序在前面的 rule match 成功的時候,match 就結束,這跟一般我們 C/C++ 預設的 operator || 行為一樣,因為 Or 底層就是這樣實做的。
所以注意下面這個例子
因為當 rule 看到第一個 'A' 的時候,rule 會把這個 'A' 消耗掉,剩下一個 'B',但是你的 grammar 已經結束了,所以 parsing 失敗。
最後一個整合的範例
上面的例子為了示範,都非常簡單,但是你其實可以用 yard 直接建立非常複雜的 rule,例如:
最後總結一下:
yard 讓你可以用高階的語法寫成 parsing rule,這點跟 boost::spirit 一樣,平心而論,template 寫起來比起 boost::spirit 的 operator overloading 是稍微麻煩了一點,但是 boost::spirit 的編譯時間實在讓人不敢恭維。
yard 本質上是一個 recursive descent parser generator,執行速度非常快,他跟你用手寫出來的 recursive descent parser 一樣好,如果你要處理的東西不需要 dynamic parsing,那 yard 會是你的第一選擇。
yard 跟 boost::spirit 一樣,支援自動的 abstract syntax tree (AST) 生成,我很想繼續寫下去,但是剛剛右下角跟我說活屍日記完檔了,所以 AST 跟怎麼在自己的程式當中使用 yard 就下次再說,先這樣囉掰。
網站 http://code.google.com/p/yardparser/
網站上面沒有什麼教學,只有下載,然後抓下來的東西裡面有 demo 程式,因為本身非常簡單,所以大致上是看了就懂,但是我還是想寫一篇教學,這樣大家可以減少一點摸索的時間。
這篇文章會先介紹 yard 跟其他 parsing gramework 的比較,然後我會從非常簡單的範例開始出發,一直到最後給一個很難的範例,讓你們熟悉如何使用 yard 建立自己的 grammar。在學會了如何使用 yard 建立自己的 grammar 之後,我會示範如何使用 yard 來自動建立 abstract syntax tree (AST),並且把 yard 使用在自己的程式裡面。
跟 boost::spirit 的比較
yard 跟 boost::spirit 一樣的地方是他們的 grammar 都不限於 LR(1),可以寫 EBNF 的 parsing rule,語法好寫好讀。boost::spirit 強勢的地方在於支援 dynamic parsing,也就是 parsing rule 可以在 runtime 動態改變。
boost::spirit 最大的弱點在於編譯效能,boost::spirit 很難用來開發大型的 parser,當你的 parsing rule 到了 30~40 以後,編譯時間就快要一個小時,一個 78 parsing rule 的 grammar,編譯要兩個小時。
執行效能也是 boost::spirit 的一個問題,每個 parsing rule 被引用的時候,都涉及一次 virtual function 的呼叫。相較於此,yard 的編譯速度非常快速!編譯完整的 c 語言語法 + XML 語法 + scheme 語法,全部加起來不用一秒。
跟 yacc 的比較
沒什麼好比的 -________-|| yacc 再見再見
簡單教學範例
好,在使用 yard 的時候,我們都要 include
// == my_grammar.hpp ==
#include <yard.hpp>
#include <yard_text_grammar.hpp>
namespace my_grammar {
using namespace yard ;
using namespace yard::text_grammar ;
/*...[[[ 這邊就是我們要寫自己的 grammar 的地方 ]]]...
}
那這篇文章之後的範例就不寫這部份了,只著重在 grammar 的地方。
yard 的設計是建立在 C++ 強大的 template 機制上面,每一個 parsing rule 就是一個 struct,藉由組合不同的 struct 來完成複雜的 grammar。
先來一個最簡單的例子
// AB
struct AB
: CharSeq < 'A', 'B' >
{} ;
這樣就完成一個可以用來 parse "AB" 這個字串的 rule 了,CharSeq 是 yard 裡面已經定義好的 struct,會從來源讀入一個一個字元來進行 match 的動作。實做細節其實很不難,但是很有創意!有興趣的去看翻一下原始碼就知道。
再來一個稍微難一點的
// (AB)*
struct ABAB
: Star < AB > // 這邊 AB 當然是接續前面的範例
{}
不用說,Star 也是 yard 裡面已經定義好的一個 struct,可以 match 零到無限多個你放在 < > 裡面當作 template 參數的規則。
馬上應用剛剛學到的兩個…
// ABC(AB)*
struct ABC_ABAB
: Seq <
CharSeq < 'A', 'B', 'C' >,
ABAB
>
{} ;
Seq 也是 yard 已經有的 struct,可以用來 match 一串序列的 rule,他跟 CharSeq 一樣都可以接不同數量的 template 參數,但是也不是沒有上限,他內部設計是 10 還是 16 我忘記了,你可以自己改,但是最後的限制會取決於你的編譯器所支援的上限。
有的時候你會想 match 多種可能的其中一種
// (AB|XYZ)
struct AB_or_XYZ
: Or <
AB,
CharSeq < 'X', 'Y', 'Z' >,
>
{} ;
當你用 Or 的時候,你就可以只 match 其中一種 rule,注意一下,當某個順序在前面的 rule match 成功的時候,match 就結束,這跟一般我們 C/C++ 預設的 operator || 行為一樣,因為 Or 底層就是這樣實做的。
所以注意下面這個例子
Or<CharSeq<'A','B'>, CharSeq<'A'>> // match "A"
Or<CharSeq<'A','B'>, CharSeq<'A'>> // match "AB"
Or<CharSeq<'A'>, CharSeq<'A','B'>> // match "A"
Or<CharSeq<'A'>, CharSeq<'A','B'>> // not match "AB"
因為當 rule 看到第一個 'A' 的時候,rule 會把這個 'A' 消耗掉,剩下一個 'B',但是你的 grammar 已經結束了,所以 parsing 失敗。
最後一個整合的範例
上面的例子為了示範,都非常簡單,但是你其實可以用 yard 直接建立非常複雜的 rule,例如:
// (+|-)?[0-9]\+(\.[0-9]\*)? 就是一般的浮點數啦
struct number
: Seq <
// (+\-)?
Opt<Or<Char<'+'>,Char<'-'>>>,
// [0-9]+
Plus<Digit>,
// (.[0-9]*)?
Opt<Seq<Char<'.'>,Star<Digit>>>
>
{} ;
最後總結一下:
yard 讓你可以用高階的語法寫成 parsing rule,這點跟 boost::spirit 一樣,平心而論,template 寫起來比起 boost::spirit 的 operator overloading 是稍微麻煩了一點,但是 boost::spirit 的編譯時間實在讓人不敢恭維。
yard 本質上是一個 recursive descent parser generator,執行速度非常快,他跟你用手寫出來的 recursive descent parser 一樣好,如果你要處理的東西不需要 dynamic parsing,那 yard 會是你的第一選擇。
yard 跟 boost::spirit 一樣,支援自動的 abstract syntax tree (AST) 生成,我很想繼續寫下去,但是剛剛右下角跟我說活屍日記完檔了,所以 AST 跟怎麼在自己的程式當中使用 yard 就下次再說,先這樣囉掰。
訂閱:
文章 (Atom)