《Imperfect C++中文版》——1.3 運行期契約:前置條件、后置條件和不變式

本節書摘來自異步社區出版社《Imperfect C++中文版》一書中的第1章,第1.3節,作者: 【美】Matthew Wilson,更多章節內容可以訪問云棲社區“異步社區”公眾號查看。

1.3 運行期契約:前置條件、后置條件和不變式

Imperfect C++中文版
“如果例程的所有前置條件(precondition)已經被調用者滿足了,那么該例程必須確保當它完成時所有后置條件(postconditions)(以及任何不變式)皆為真。”——Hunt and Thomas, The Pragmatic Programmers [Hunt2000]。

如果我們無法執行編譯期強制,那么還可以采用運行期強制。運行期強制的一個系統化的實現途徑是指定函數契約。函數契約精確定義了在函數被調用之前調用者必須滿足哪些條件(前置條件),以及在函數返回之時哪些條件(后置條件)是調用者可以期望的。契約的定義以及它們的強制實施是DbC(Design by Contract,契約式設計)[Meye1997]的基石。

前置條件是指函數履行其契約所必須滿足的條件。滿足前置條件是調用者的責任,而被調用者則假定它的前置條件已經被滿足,并且僅當它的前置條件被滿足時才負責提供正確的行為。這一點非常重要,在[Meye 1997]中被強調指出。倘若調用者沒有滿足前置條件,則被調用者做出任何事情都是完全合理的。事實上,通常這會引發一個斷言(見1.4節),進而可能導致程序終止。這聽起來似乎頗令人恐慌,剛接觸DbC的程序員通常會對此感到很不舒服,直到你問起他們:如果一個函數的(前置)條件都不能被滿足,那還能指望它有什么樣的行為時,他們才啞口無言。事實上,契約越嚴格,違反它所導致的后果越嚴重,從而軟件的質量就會越好。當轉到DbC上時,要理解這一點是最為困難的。

后置條件在函數執行完畢時必須為真。確保后置條件被滿足是被調用者的責任。當函數返回控制時調用者可以假定后置條件已經得到了滿足。在現實中,有些時候有所保留(不要把賭注全部押在被調用者身上)還是必要的,例如,當調用應用服務器中的第三方插件時就是如此。然而,我認為前面所講的原則仍然是對的。事實上,對違反契約的插件的合理反應之一是將它卸載掉,并給公司經理以及第三方插件廠商發一封電子郵件。既然我們對于違反契約的行為可以作出任何反應,那么有什么理由不這么做呢?

前置條件和后置條件可以被應用到類的成員函數,也可以被用到自由函數身上,這對于C++(更一般地說,面向對象編程)來說很有益處。事實上,還有另外一個與DbC相關的東西,它只能依附于類而存在,那就是類不變式(class invariant)。類不變式是指一個或一組條件式,它們對于一個處于良好定義狀態的對象總是為真。根據定義,類的構造函數負責確保類的實例進入一個符合該類的不變式的狀態中,而類的(public)成員函數則在它們完成之際確保類的實例仍然處在該狀態中。僅當處于構造函數、析構函數或其他某個成員函數的執行過程中時,類不變式才不一定要為真。

在某些場合下,將不變式的作用范圍定義為比“單個對象的狀態”的范圍更廣可能更合適一些。原則上,不變式可以被應用到操作環境的整個狀態上,然而,在實踐中,這種情況是極其少見的,類不變式則很常見。因此,在本章以及本書剩余的篇幅中,如果提到不變式,均是指類不變式。

對部分或根本沒有進行封裝的類型提供不變式是可行的(見3.2節和4.4.1小節),這個不變式是由與該類型相關的API函數(以及該函數的前置條件)來強制實施的。事實上,當使用這種類型時,不變式是極好的主意,因為它們缺乏封裝性的特質提高了濫用的風險。不過這種不變式相當容易被“繞過”,這也說明了為什么通常應該避免使用這種類型。事實上,[Stro2003]中某種程度上提到:如果存在一個不變式,則公有數據簡直毫無意義。封裝既是關于隱藏實現又是關于保護不變式的。至于“屬性”(第35章),可能是為了結構上的一致性(見20.9節)而引入的,只不過為我們提供公有成員變量的表象而已,它仍然具有不變式。

對于違反前置條件、后置條件或者不變式,你所采取的行動完全由你來決定。你可以把信息記錄到日志文件中,也可以拋出異常,或者給你家人發一封SMS,告訴她今夜你將debug到很晚。不過,通常我們采取的行動是引發一個斷言。

1.3.1 前置條件

在C++中,前置條件測試相當簡單。在這本書中我們已經看到了好幾個例子。它和使用斷言一樣簡單:

template< . . . >
typename pod_vector<. . .>::reference pod_vector<. . .>::front()
{MESSAGE_ASSERT("Vector is empty!", 0 != size());assert(is_valid());return m_buffer.data()[0];
}

1.3.2 后置條件

這是C++容易產生磕磕碰碰的地方。這里的挑戰是在函數的退出點捕獲返回值和“輸出”參數。1當然了,C++提供了特別有用的RAII(Resource Acquisition Is Initialization,資源獲取即初始化)機制(見3.5節),該機制保證當執行流程退出某個作用域時棧上對象的析構函數都會得到調用。這就意味著我們可能借助這一點實現一個可行方案,至少該機制具備這個潛力。

我們的選擇之一是聲明監視器對象,它持有對輸出參數和返回值的引用。

int f(char const *name, Value **ppVal, size_t *pLen)
{int                 retVal;retval_monitor    rvm(retVal, . . . policy . . . );outparam_monitor  opm1(ppVal, . . . policy . . . );outparam_monitor  opm2(pLen, . . . policy . . . );. . . // 函數體return retVal;
}

一些策略會被用來檢查變量是否為NULL,或者是否位于一個特定的區間內,或者是一組數值中的一個,等等。盡管實現這些東西都有困難,這里仍然存在兩個問題。第一,rvm的析構函數會對它所持有的指向函數返回值變量retVal的引用來施行約束。如果函數的其他任何部分返回了一個不同的值(或一個常量),那么rvm無可避免地會報告一次失敗。為了能夠正確工作,我們不得不強制讓所有函數都通過單個變量來返回,這肯定不符合一些人的口味,在某些場合下也是不可能的。

然而,最主要的問題還在于各個后置條件監視器之間是沒有關聯的。大多數函數的后置條件是復合型的,個體輸出參數和返回值僅當符合某種一致的關系時才有意義,例如:

assert(retVal > 0 || (NULL == *ppVal && 0 == *pLen));

我不打算建議你如何將這3個個體監視器對象以這樣的方式結合起來,以便強制實施各種各樣的后置條件狀態,這類事情對于模板元編程愛好者可能是一個令人激動的挑戰,不過對于其他人,它所帶來的復雜性不值得我們付出代價。

Imperfection: C++對后置條件未提供合適的支持。
在我看來,惟一合理的(雖然看起來很平凡)解決方案是,通過一個轉發函數將(待調用)函數和對它的(后置條件)檢查分離開來,就像在程序清單1.5中展示的那樣:

程序清單1.5

int f(char const *name, Value **ppVal, size_t *pLen)
{. . . // 進行f()的前置條件檢查int retVal = f_unchecked(name, ppVal, pLen);. . . // 進行f()的后置條件檢查return retVal;
}
int f_unchecked(char const *name, Value **ppVal, size_t *pLen)
{. . . // f的語義
}

在實際代碼中,你可能希望在不需要執行DbC的地方省略掉所有的檢查,為此我們需要使用預處理器:

程序清單1.6

int f(char const *name, Value **ppVal, size_t *pLen)
#ifdef ACMELIB_DBC
{. . . // 進行f()的前置條件檢查int retVal = f_unchecked(name, ppVal, pLen);. . . // 進行f()的后置條件檢查return retVal;
}
int f_unchecked(char const *name, Value **ppVal, size_t *pLen)
#endif /* ACMELIB_DBC */
{. . . // f的語義
}

這完全算不上優雅,不過它可以工作,并可以很容易地合并到代碼生成器中。當處理被重寫的(overridden)類成員函數時,問題可能要稍微復雜一點,因為你要面對是否實施父類的前置條件和后置條件的問題。這得條分縷析后才能決定,已經超出了我們的討論范圍。2

1.3.3 類不變式

在C++中,實現類不變式幾乎和實現前置條件一樣簡單。我個人的做法是為類定義一個名為is_valid()的方法,像這樣:

template<. . . >
inline bool pod_vector<. . .>::is_valid() const
{if(m_buffer.size() < m_cItems){return false;}. . . // 這里進行進一步的檢查return true;
}

然后,該類的每個公有方法都把它放在斷言里進行調用,在進入方法時斷言一次,退出方法前再來一次。我喜歡在緊接著前置條件檢查之后進行類不變式的檢查(見1.3.1小節):

template< . . . >
inline void pod_vector<. . .>::clear()
{assert(is_valid());m_buffer.resize(0);m_cItems = 0;assert(is_valid());
}

作為一種替代策略,我們可以將斷言放在不變式函數自身之中。然而,除非你手頭擁有的是一個“久經考驗”的斷言(見1.4節),否則這會令你不得不選擇提供關于“肇事”的條件或方法的斷言信息(文件+行+消息)。我傾向于后者,因為違反不變式畢竟是非常少見的情況。不過,你可能會選擇前者,如果是那樣的話,你可能希望將斷言放到is_valid()成員函數中。

事實上,對此存在一個合理的折中方案,我通常在具有良好的日志/跟蹤界面的環境中使用這種策略(見21.2節),具體做法是在is_valid()成員函數中記錄違反不變式的細節,并且讓“肇事”成員函數3來觸發該斷言。

與輸出參數和返回值檢查不同,使用RAII(見3.5節)來使類不變式的檢查自動化還是相當容易的(這種檢查也作為方法退出前的后置條件驗證的一部分),像這樣:

template< . . . >
inline void pod_vector<. . .>::clear()
{check_invariant<class_type> check(this);m_buffer.resize(0);m_cItems = 0;
}

缺點是,強制會在check_invariant模板實例的構造函數和析構函數中被實施,這意味著使用預處理器來獲悉  FILE 和 LINE 信息的簡單的斷言可能會給出誤導信息。然而,要想實現一個可以正確顯示斷言失敗位置的“宏+模板”的斷言形式并不算是很大的挑戰,甚至可以結合運用非標準的 FUNCTION 預處理符號(當然,對于那些支持它的編譯器而言)。

1.3.4 檢查?總是進行

在[Stro2003]中,Bjarne Stroustrup做了一個非常重要的觀察:不變式只對那些具有方法的類才是必要的,而對于僅僅作為變量聚合體的簡單結構而言是沒有必要的(例如,我們將會在4.4.2小節看到的Patron類型就不需要不變式)。在我看來,這話還可以這么說:任何具有方法的類都應該具有類不變式。不過,在實踐中對此有一個下限。如果你的類持有一個指向某些資源的指針,那么,它要么是NULL,要么不是NULL。除非你的類不變式方法可以使用非空指針所指向的有效的外部資源,否則你的類不變式將無事可干。在這種情況下,是否使用一個“存根(stub)”類不變式取決于你自己,或者你也可以干脆什么都不干。但如果你的類將來會不斷升級,那么在里面放上一塊有待以后擴充的“存根”方法可以令后續的精化工作變得容易一些。如果你使用了某種代碼生成器的話,我建議你總是用它來生成類不變式,并生成對所生成的類不變式的調用。

類不變式較之散落在類實現周圍的一堆斷言而言,好處是非常明顯的。類不變式使你的代碼更容易閱讀,并且在不同的類的實現之間具有一致的外觀,以及具有更好的可維護性,這是因為對于每個類你都把類不變式定義在了某個單一的地方。

1.3.5 DbC還是不DbC

到目前為止,我所描繪的關于運行期契約的藍圖其實隱含了一個假定,那就是:在進行適當的測試后,人們會對他們的系統進行一次構建(build),4在這次構建中,DbC元素都被預處理器消去。5

事實上,關于“是否任何構建(build)都應該不實施DbC”這個問題[Same2003],仍然頗有爭議。一個論據是(借用[Same2003]里的邏輯)DbC里的契約實施就好比電力系統中的保險絲,任何人都不應該在部署一個成熟的電力設備之前把它里面的所有保險絲都拔掉。

斷言和保險絲之間的區別在于前者涉及運行期測試,而測試的代價明顯不為零。盡管保險絲中的合金成分的電阻可能與它所在系統中的其他部分的電阻略有差別,然而這跟斷言引入的代價相比仍然無法相提并論。我的看法是,這需要仔細分析才能求得一個良好的平衡。這就是為什么本節的例子代碼中包含了ACMELIB_DB這個符號的緣故。我沒有使用NDEBUG(或者_DEBUG),因為DbC的使用不應該直接和“調試版/發行版(debug/release)”的二進制概念耦合起來。究竟何時使用它,何時消除它,取決于你自己。6

1.3.6 運行期契約:尾聲

盡管我們已經看到C++在后置條件方面是有缺陷的,然而進行前置條件和類不變式的測試仍然是合理的。在實踐中,將這兩者結合使用往往能發揮DbC大部分的威力。對返回值和輸出參數的后置條件測試的能力缺失雖然令人遺憾,但也并非十分嚴重的事情。如果你必需這種能力的話,你可以求助于預處理器,就像在1.3.2小節中看到的那樣。

如同約束一樣, 對于不變式,我們可以通過使用一個間接層讓日子好過一些。這個間接層對于約束來說是一個宏,而對于不變式來說則是一個成員函數。正因為如此,提供對新的編譯器的支持或者修改某個類的內部實現也變得更為容易了,并且,我們還把該機制不爽的那一面全部隱藏到了類不變式方法中。

1譯者注:即用于向外界返回東西的函數參數,例如指向待填充的緩沖區的指針。
2在這一點上,我承認我有點膽小自私,不過我有很好的借口。即便是在成熟運用DbC的語言中,對于繼承體系中的層與層之間的關聯契約的用處(事實上是機制)仍然是模棱兩可的。此外,為C++加入DbC的提議直到本書的撰寫時仍然不過是納入考慮而已[Otto2004],因此,我認為在這里過多地在細節上饒舌沒有什么好處。
3譯者注:而非不變式函數。
4譯者注:對程序進行編譯和連接的過程。
5譯者注:其實通常就是發行版(release)的構建,其中assert(exp)會展開為空。
6在ISE Eiffel 4.5中,你無法去掉前置條件,大概是因為前置條件可以在程序變成未定義狀態之前進行反饋,從而對于程序捕獲違反前置條件的異常并繼續執行是有意義的。
本文僅用于學習和交流目的,不代表異步社區觀點。非商業轉載請注明作譯者、出處,并保留本文的原始鏈接。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/541497.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/541497.shtml
英文地址,請注明出處:http://en.pswp.cn/news/541497.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python名稱空間與運用域_Python名稱空間和作用域講座,命名,Namespaces,Scopes

Python命名空間(Namespaces)和作用域(Scopes)講座命名空間(Namespace)命名空間(Namespace)&#xff0c;是名稱到對象的映射。命名空間提供了在項目中避免名字沖突的一種方法。命名空間是獨立的&#xff0c;沒有任何關系的&#xff0c;所以一個命名空間中不能有重名&#xff0c;…

getminimum_Java Calendar getMinimum()方法與示例

getminimumCalendar類的getMinimum()方法 (Calendar Class getMinimum() method) getMinimum() method is available in java.util package. getMinimum()方法在java.util包中可用。 getMinimum() method is used to get the minimum value of the given field(fi) of this Cal…

《Spark核心技術與高級應用》——3.2節構建Spark的開發環境

本節書摘來自華章社區《Spark核心技術與高級應用》一書中的第3章&#xff0c;第3.2節構建Spark的開發環境&#xff0c;作者于俊 向海 代其鋒 馬海平&#xff0c;更多章節內容可以訪問云棲社區“華章社區”公眾號查看 3.2 構建Spark的開發環境無論Windows或Linux操作系統&am…

python閉包怎么理解_Python 閉包的理解

Last Updated on 2019年10月15日Python中的閉包是一個比較模糊的概念&#xff0c;不太好理解&#xff0c;我最近的面試中也被問及&#xff0c;在一個單例模式的實現上&#xff0c;我用裝飾器實現單例&#xff0c;然后面試官就問到了我對閉包的理解&#xff0c;回答的不太清楚。…

Java BufferedReader mark()方法與示例

BufferedReader類mark()方法 (BufferedReader Class mark() method) mark() method is available in java.io package. mark()方法在java.io包中可用。 mark() method is used to mark the current position in this stream and whenever we call reset() method so it will re…

《全球互聯網金融商業模式:格局與發展》——第3章,第3節互聯網保險公司...

本節書摘來自華章出版社《全球互聯網金融商業模式&#xff1a;格局與發展》一書中的第3章&#xff0c;第3.3節互聯網保險公司&#xff0c;作者廖理&#xff0c;更多章節內容可以訪問云棲社區“華章計算機”公眾號查看 3.3 互聯網保險公司互聯網思維貫穿整個保險創新發展過程&a…

webapi隨機調用_BeetleX之webapi驗證插件JWT集成

對于webapi服務應用很多時候需要制訂訪問限制&#xff0c;在前面的章節也講述了組件如何制訂控制器訪問控制&#xff1b;但到了實際應用要自己去編寫還是比較麻煩。為了讓訪問控制更方便組件實現基于JWT的控制器訪問控制組件BeetleX.FastHttpApi.Jwt&#xff1b;通過這個組件可…

java bitset_Java BitSet nextClearBit()方法與示例

java bitsetBitSet類nextClearBit()方法 (BitSet Class nextClearBit() method) nextClearBit() method is available in java.util package. nextClearBit()方法在java.util包中可用。 nextClearBit() method is used to retrieve the index of the first bit that is set to …

《馴獅記——Mac OS X 10.8 Mountain Lion使用手冊》——2.3 Dock

本節書摘來自異步社區《馴獅記——Mac OS X 10.8 Mountain Lion使用手冊》一書中的第2章&#xff0c;第2.3節&#xff0c;作者&#xff1a;陳明 , 張錚 , 馬玉龍著&#xff0c;更多章節內容可以訪問云棲社區“異步社區”公眾號查看 2.3 Dock 馴獅記——Mac OS X 10.8 Mountain…

mysql 嵌套if標簽_對比Excel、MySQL、Python,分別講述 “if函數” 的使用原理!

作者&#xff1a;黃偉呢本文轉自&#xff1a;數據分析與統計學之美其實&#xff0c;不管是Excel、MySQL&#xff0c;還是Python&#xff0c;“if”條件判斷都起著很重要的作用。今天這篇文章&#xff0c;就帶著大家盤點一下&#xff0c;這三種語言如何分別使用 “if函數” 。if…

Java BigDecimal intValue()方法與示例

BigDecimal類的intValue()方法 (BigDecimal Class intValue() method) intValue() method is available in java.math package. intValue()方法在java.math包中可用。 intValue() method is used to convert a BigDecimal to an integer and when the converted BigDecimal val…

R語言數據挖掘

數據分析與決策技術叢書 R語言數據挖掘 Learning Data Mining with R &#xff3b;哈薩克斯坦&#xff3d;貝特麥克哈貝爾&#xff08;Bater Makhabel&#xff09; 著 李洪成 許金煒 段力輝 譯 圖書在版編目&#xff08;CIP&#xff09;數據 R語言數據挖掘 / &#xff08;哈…

linux adduser mysql_linux_adduser

新帳號建立當不加-D參數,useradd指令使用命令列來指定新帳號的設定值and使用系統上的預設值.新使用者帳號將產生一些系統檔案&#xff0c;使用者目錄建立&#xff0c;拷備起始檔案等&#xff0c;這些均可以利用命令列選項指定。此版本為RedHatLinux提供&#xff0c;可幫每個新加…

java iterator_Java ArrayDeque iterator()方法與示例

java iteratorArrayDeque類iterator()方法 (ArrayDeque Class iterator() method) iterator() Method is available in java.lang package. iterator()方法在java.lang包中可用。 iterator() Method is used to return an iterator over the deque elements. iterator()方法用于…

《jQuery、jQuery UI及jQuery Mobile技巧與示例》——7.4 示例:使用按鈕集裝飾單選框...

本節書摘來自異步社區《jQuery、jQuery UI及jQuery Mobile技巧與示例》一書中的第7章&#xff0c;第7.4節&#xff0c;作者&#xff1a;【荷】Adriaan de Jonge , 【美】Phil Dutson著&#xff0c;更多章節內容可以訪問云棲社區“異步社區”公眾號查看 7.4 示例&#xff1a;使…

mysql 模擬序列_【原創】MySQL 模擬PostgreSQL generate_series 表函數

PostgreSQL 提供了一個很強大的造數據的函數generate_series&#xff0c;基于Common Table Expression。MySQL 沒有復雜的應用程序類型&#xff0c;該如何實現這樣的功能呢&#xff1f; 我想到的三種方法如下:1. 用存儲過程來做。 缺點是寫好多數據庫不擅長的應用邏輯。2. 我們…

Python字符串| isdigit()方法與示例

isdigit() is an in-built method in Python, which is used to check whether a string contains only digits or not. isdigit()是Python中的內置方法&#xff0c;用于檢查字符串是否僅包含數字。 Digit value contains all decimal characters and other digits which may …

vue2.0的學習

vue-router 除了使用 <router-link> 創建 a 標簽來定義導航鏈接&#xff0c;我們還可以借助 router 的實例方法&#xff0c;通過編寫代碼來實現。 1&#xff09;router.push(location) 這個方法會向 history 棧添加一個新的記錄&#xff0c;所以&#xff0c;當用戶點擊瀏…

mysql+url的配置參數詳解_MySql鏈接url參數詳解

mysql URL格式如下&#xff1a;jdbc:mysql://[host:port],[host:port].../[database][?參數名1][參數值1][&參數名2][參數值2]...MySQL在高版本需要指明是否進行SSL連接 在url后面加上 useSSLtrue 不然寫程序會有warning常用的幾個較為重要的參數&#xff1a;參數名…

Java LocalDate類| minus()方法與示例

LocalDate類isSupported()方法 (LocalDate Class isSupported() method) Syntax: 句法&#xff1a; public LocalDate minus(TemporalAmount t_amt);public LocalDate minus(long amt, TemporalUnit t_unit);isSupported() method is available in java.time package. isSuppo…