本節書摘來自異步社群出版社《imperfect c++中文版》一書中的第1章,第1.2節,作者: 【美】matthew wilson,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
imperfect c++中文版
本章講述編譯期強制,通常它也被稱為“限制(constraints)”。遺憾的是,c++并不直接支援限制。
imperfection: c++ 不直接支援限制。
c++是一門極其強大和靈活的語言,是以很多支援者(甚至包括一些c++權威)都會認為本節描述的限制實作技術已經足夠了。然而,作為c++和限制的雙重擁護者,我必須提出我的異議(由于一些很平常的原因)。雖然我并不買其他語言鼓吹者的賬,然而我同樣認為閱讀因違反限制而導緻的編譯錯誤資訊是很困難(甚至極其困難)的。它們通常令人眼花缭亂,有時甚至深奧無比。如果你是“肇事”代碼的作者,那麼問題通常并不嚴重,然而,當面對的是一個多層模闆執行個體化的情形時,其失敗所導緻的錯誤資訊近乎天書,難以了解,甚至非常優秀的編譯器都是如此。本節後面我們會看到一些限制,以及違反它們所導緻的錯誤資訊,還有可以令錯誤資訊稍微更具可讀性的一些措施。
這個例子是從comp.lang.c++.moderated新聞討論區上幾乎原樣照搬過來的,是bjarne stroustrup發的文章,他稱之為“has base”。[sutt2002]對此亦有描述,不過換了個名字,叫做isderivedfrom。而我則喜歡将限制的名字以“must_”開頭,是以我把它命名為must_have_base。
程式清單1.1
它的工作原理如下:在成員函數constraints()中嘗試把d類型的指針指派給b類型的指針(d和b都是模闆參數),constraints()是一個單獨的靜态成員函數,這是為了確定它永遠不會被調用,是以這種方案沒有任何運作期負擔。析構函數中則聲明了一個指向constraints()的指針,進而強迫編譯器至少要去評估該函數以及其中的指派語句是否有效。
事實上,這個限制的名字有點不恰當:如果d和b是同樣的類型,那麼這個限制仍然能夠被滿足,是以,它或許應該更名為must_have_base_or_be_same_type,或者類似這樣的名字。另一個替代方案是把must_have_base進一步精化,令它不接受d和b是同一類型的情況。請将你的答案寫在明信片上寄給我。
另外,如果d和b的繼承關系并非公有繼承,那麼該限制也會失敗。在我看來,這是個命名方式的問題,而不是限制本身的能力缺陷問題,因為我隻需要對采用公有繼承的類型應用這個 限制。1
由于“must_have_base”在其定義中所進行的動作恰好展現了限制本身的語義,是以如果該限制失敗,錯誤資訊會相當直覺。事實上,我手頭所有編譯器(見附錄a)對此均提供了非常有意義的資訊,即要麼提到兩個類型不具有繼承關系,要麼提到兩個指針類型不能互相轉換,或與此類似的資訊。
另一個有用的限制是要求一個類型可以按下标方式通路(見14.2節),實作起來很簡單:
為了提高可讀性,constraints()的形參被命名為t_is_not_subscriptable,這可以為那些違反限制的可憐的人們提供些許線索。考慮下面的例子:
borland 5.6給出了令人難以置信的資訊:“'operator+' not implemented in type '' for arguments of type 'int' in function must_be_subscriptable::constraints(const not_subs &)”。而當模闆執行個體化深度多達15層時,不絞盡腦汁簡直是不可能搞清楚這些錯誤資訊的含義的。
相對而言,digital mars更為準确一些,不過仍然不夠好:“error: array or pointer required before '['; had: const not_subs”。
另外一些編譯器的錯誤資訊都包含了變量名“t_is_not_subscriptable”。其中最優秀的當數visual c++給出的錯誤資訊:“binary '[' : 'const struct not_subs' does not define this operator or a conversion to a type acceptable to the predefined operator while compiling class-template member function 'void must_be_subscriptable::constraints (const struct not_subs &)”。
在第14章中,我們會深入考察數組和指針之間的關系,并了解有關指針偏移的一些詭秘的特性,它緻使offset[pointer]這種文法成為跟pointer[offset]一樣合法且等價的形式(這也許使borland編譯器關于must_be_subscriptable的莫名其妙的編譯錯誤看起來不是那麼毫無意義,不過它對于我們追蹤并了解限制是如何被違反的确實有很大的幫助)。由于這種颠倒形式對于重載了operator []的類(class)類型是無效的,是以must_be_subscriptable還可以被進一步精化,進而把它作用的類型限制到僅限于(原生)指針類型。
程式清單1.2
很明顯,所有可以通過offset[pointer]形式進行索引通路的pointer也都可以接受pointer[offset]操作,是以不需要把must_be_subscriptable合并到must_be_subscriptable_as_decayable_pointer中去。不過,盡管這兩種限制有着不同的結果,但利用繼承機制将二者結合起來也是合适之舉。
現在我們可以區分(原生)指針和其他可進行索引通路的類型了:
你會在本書中多次看到must_be_pod()的使用(見19.5、19.7、21.2.1和32.2.3節)。這是我編寫的第一個限制,那時我根本不知道限制是什麼,甚至連pod是什麼意思都不清楚(見“序”)。must_be_pod()非常簡單。
c++98标準9.5節第1條說明:“如果一個類具有非平凡的(non-trivial)構造函數、非平凡的拷貝構造函數、非平凡的析構函數,或者非平凡的指派操作符,那麼其對象不能作為聯合(union)的成員”。這恰好滿足我們的需要,并且,我們還可以設想,這個限制和我們已經看到的那些模樣差不多:有一個constraints()成員函數,其中包含一個union:
遺憾的是,這是編譯器容易發生奇怪行為的領域,是以真正的定義沒有這麼簡單,而是需要許多預處理器操作(見1.2.6節),但效果還是一樣的。
在19.7節中,我們将會看到這個限制和一個更專門化的限制must_be_pod_or_void()一起使用,目的在于能夠檢查某個指針所指的是否為非平凡的類類型。為此,我們需要對must_be_pod_or_void模闆進行特化[vand2003],而其泛化的定義則與must_be_pod相同:
同樣,編譯器對于違反must_be_pod / must_be_pod_or_void限制所生成的資訊也是各式各樣的:
這一次,digital mars一慣的簡練風格卻給我們帶來了麻煩,因為我們能得到的錯誤資訊隻是“error: union members cannot have ctors or dtors”,指向限制類内部引發編譯錯誤的那行代碼。如果這是在一個大項目裡的話,那麼很難追溯到錯誤的源頭,即最初引發這個錯誤的執行個體化點。而watcom對于這麼一個極小的錯誤給出的資訊則是最多的:“error! e183: col(10) unions cannot have members with constructors; note! n633: col(10) template class instantiation for ''must_be_pod< nonpod>' was in: ..constraints_test.cpp(106) (col 48)”。
最後一個限制must_be_same_size()也在本書的後續部分被使用到(見21.2.1小節和25.5.5小節)。該限制類使用靜态斷言“無效數組大小”技術來確定兩個類型的大小是一緻的,我們很快就會在1.4.7節看到該技術。
程式清單1.3
如果兩個類型大小不一緻,那麼t1_not_same_size_as_t2就會被求值為(編譯期)常數0,而将0作為數組大小是非法的。
至于must_be_pod_or_void,則是在兩個類型中有一個或都是void時會用到它。然而,由于sizeof(void)是非法的表達式,是以我們必須提供一些額外的編譯期功能。
如果兩個類型都是void,則很容易,我們可以這樣來特化must_be_same_size類模闆:
然而,如果隻有一個類型是void,要使其工作可就沒那麼直截了當了。解決方案之一是利用局部特化機制[vand2003],然而并非所有目前廣為使用的編譯器都支援這種能力。進一步而言,我們還得同時為這個模闆準備一個完全特化和兩個局部特化版本(兩個局部特化分别将第一個和第二個模闆參數特化為void)。最後,我們還得構思某種方式來提供哪怕是“有點兒”意義的編譯期錯誤資訊。我沒有借助于這種方法,而是通過令void成為“可size_of的”來解決這個問題。我的方案實作起來極其容易,并且不需要局部特化:
程式清單1.4
現在我們所要做的就是在must_be_same_size中用size_of來替代sizeof:
現在我們可以對任何類型進行驗證了:
正如前面的限制所表現出來的,不同的編譯器提供給程式員的資訊量有相當大的差别。borland和digital mars在這方面又敗下陣來,它們提供的上下文資訊極少甚至沒有。在這方面,我認為intel做得最好,它提到“zero-length bit field must be unnamed”,指出出錯的行,并且提供了兩個直接調用上下文,其中包括t1的實際類型和t2的實際類型,加在一起一共4行編譯器輸出。
我比較喜歡通過宏來使用我的限制,宏的名字遵循“constraint_”的形式,2例如constraint_must_have_base()。這從多方面來說都是非常有意義的。
首先,容易無歧義地查找到它們。正因為如此,我才為限制保留了“must_”字首。也許有人會争論說這個需求已經被滿足了。然而使用宏的做法更具有“自描述”性。對觀者而言,在代碼中看到“constraint_must_be_pod()”,其意義更加明确。
其次,使用宏的形式更具有(文法上的)一緻性。盡管我沒有寫過任何非模闆的限制,然而并沒有任何理由限制其他人那麼做。此外,我發現尖括号除了導緻眼睛疲勞外,沒有什麼其他 好處。
再次,如果限制被定義在某個名字空間中,那麼在使用它們時必須加上冗長的名字限定。而宏則可以輕易地将名字限定隐藏起來,免得使用者去使用淘氣的using指令(見34.2.2小節)。
最後一個原因更實際。不同的編譯器對相同的限制的處理具有細微的差别,為此需要對它們耍弄一些手段。例如,針對不同的編譯器,constraint_must_be_pod()被定義成如下3種形式之一:
利用宏,客戶代碼變得更加簡潔優雅,否則它們會遭受大量的面目醜陋的代碼的幹擾。
1.2.7 限制和tmp
本書的一位審稿人提到部分限制可以通過tmp(template meta-programming,模闆元程式設計)1實作,他說的很對。例如,must_be_pointer限制就可以通過結合運用靜态斷言(見1.4.7小節)和is_pointer_type(見33.3.2小節)來實作。如下:
我之是以不采用tmp有幾方面原因。首先,限制的編碼看上去總是那麼直覺,因為一個限制隻不過是對被限制類型所應該具備的行為的“模拟”。而對于tmp traits就不能這麼說了,有些tmp traits可能會相當複雜。是以,限制較之模闆元程式設計更易于閱讀。
其次,在許多(盡管并非全部)情況下,要想“說服”編譯器給出容易了解的資訊,限制比traits(和靜态斷言)更容易一些。
最後,有些限制無法通過tmp trait來實作,或者至少隻能在很少一部分編譯器上實作。甚至可以說,限制越是簡單,用tmp traits來實作它就越困難,對此must_be_pod就是一個極好的例子。
herb sutter在[sutt2002]中曾示範了結合運用限制和traits的技術,當你在進行自己的實際工作中時,沒有任何理由阻止你那麼做。對我來說,我隻不過更喜歡保持它們簡潔且獨立而已。
本章所講述的限制當然并非全部,然而,它們可以告訴你哪些東西是可實作的。限制的不足之處和靜态斷言一樣(見1.4.8小節),那就是産生的錯誤資訊不是特别容易了解。根據實作限制的機制的不同,你可能會看明白如“type cannot be converted”之類的錯誤資訊,也可能會看到令人極其困惑的“destructor for 't' is not accessible in function ”之類的資訊。
隻要有可能,你應該通過為你的限制選擇合适的變量或常量名字來改善錯誤資訊。我們在本節已經看到了這方面的例子:t is not subscriptable、t is not pod type以及t1not_same_size as_t2。記住,要確定你選擇的名字反映了出錯的情況。想想違反了你的限制卻看到諸如“t is valid type for constraint”之類的資訊的可憐人吧!
最後,關于限制,還有一個值得強調的重要方面:随着我們對編譯期計算和模闆元程式設計的認識越來越清晰,我們可能會想去更新某些限制。也許你已經從本書中描述的某些元件中看出我并非模闆元程式設計方面的大師,不過重點是,通過良好地設計用于表現和使用限制的方式,我們可以在以後學到更多的技巧時再來“無縫”地更新我們的限制。我承認我自己有好多次就是這麼做的,我不覺得這是什麼丢人的事情。不過,我早期在編寫限制方面的嘗試如果拿出來現眼倒的确令人赧顔(我沒有在附錄b中提到它們,因為它們還沒有差勁到那個地步,遠遠沒有)!
1這聽起來像是循環論證,不過我敢保證它不是。
2正如12.4.4小節所描述的原因,把宏命名為大寫形式總是好習慣。之是以我對限制的宏沒有采用同樣的命名習慣,是因為我想要和限制類型保持一緻(小寫)。事後我才發現這麼做令它們看起來不怎麼顯眼。你自己編寫的限制當然可以采用大寫風格。
本文僅用于學習和交流目的,不代表異步社群觀點。非商業轉載請注明作譯者、出處,并保留本文的原始連結。