• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            天行健 君子當自強而不息

            【ZT】C++批判(1)

            以下文章翻譯自Ian Joyner所著的
            《C++?? A Critique of C++ and Programming and Language Trends of the 1990s》 3/E【Ian Joyner 1996】

            該篇文章已經包含在Ian Joyner所寫的《Objects Unencapsulated 》一書中(目前已經有了日文的翻譯版本),該書的介紹可參見于:
            http://www.prenhall.com/allbooks/ptr_0130142697.html
            http://efsa.sourceforge.net/cgi-bin/view/Main/ObjectsUnencapsulated
            http://www.accu.org/bookreviews/public/reviews/o/o002284.htm


            虛擬函數

              在所有對C++的批評中,虛擬函數這一部分是最復雜的。這主要是由于C++中復雜的機制所引起的。雖然本篇文章認為多態(polymorphism)是實現面向對象編程(OOP)的關鍵特性,但還是請你不要對此觀點(即虛擬函數機制是C++中的一大敗筆)感到有什么不安,繼續看下去,如果你僅僅想知道一個大概的話,那么你也可以跳過此節。【譯者注:建議大家還是看看這節會比較好】

             在C++中,當子類改寫/重定義(override/redefine)了在父類中定義了的函數時,關鍵字virtual使得該函數具有了多態性,但是 virtual關鍵字也并不是必不可少的(只要在父類中被定義一次就行了)。編譯器通過產生動態分配(dynamic dispatch)的方式來實現真正的多態函數調用。

              這樣,在C++中,問題就產生了:如果設計父類的人員不能預見到子類可能會改寫哪個函數,那么子類就不能使得這個函數具有多態性。這對于C++來說是一個很嚴重的缺陷,因為它減少了軟件組件(software components)的彈性(flexibility),從而使得寫出可重用及可擴展的函數庫也變得困難起來。

             C++同時也允許函數的重載(overload),在這種情況下,編譯器通過傳入的參數來進行正確的函數調用。在函數調用時所引用的實參類型必須吻合被重載的函數組(overloaded functions)中某一個函數的形參類型。重載函數與重寫函數(具有多態性的函數)的不同之處在于:重載函數的調用是在編譯期間就被決定了,而重寫函數的調用則是在運行期間被決定的。

             當一個父類被設計出來時,程序員只能猜測子類可能會重載/重寫哪個函數。子類可以隨時重載任何一個函數,但這種機制并不是多態。為了實現多態,設計父類的程序員必須指定一個函數為virtual,這樣會告訴編譯器在類的跳轉表(class jump table)【譯者竊以為是vtable,即虛擬函數入口表】中建立一個分發入口。于是,對于決定什么事情是由編譯器自動完成,或是由其他語言的編譯器自動完成這個重任就放到了程序員的肩上。這些都是從最初的C++的實現中繼承下來的,而和一些特定的編譯器及聯結器無關。

             對于重寫,我們有著三種不同的選擇,分別對應于:“千萬別”,“可以”及“一定要”重寫:

             1、重寫一個函數是被禁止的。子類必須使用已有的函數。
             2、函數可以被重寫。子類可以使用已有的函數,也可以使用自己寫的函數,前提是這個函數必須遵循最初的界面定義,而且實現的功能盡可能的少及完善。
             3、函數是一個抽象的函數。對于該函數沒有提供任何的實現,每個子類都必須提供其各自的實現。
             
             父類的設計者必須要決定1和3中的函數,而子類的設計者只需要考慮2就行了。對于這些選擇,程序語言必須要提供直接的語法支持。
             
            選項1:
             
             C ++并不能禁止在子類中重寫一個函數。即使是被聲明為private virtual的函數也可以被重寫?!維akkinen92】中指出了即使在通過其他方法都不能訪問到private virtual函數,子類也可以對其進行重寫。

            如下所示,將輸出:class B

            #include <stdio.h>

            class A
            {
            private:
                virtual void f()
                {
                    printf("class A\n");
                }

            public:
                void call_f()
                {
                    f();
                }
            };

            class B : public A
            {
            public:
                void f()
                {
                    printf("class B\n");
                }
            };


            int main()
            {
                B b;
                A* a = &b;

                a->call_f();

                return 0;
            }


            實現這種選擇的唯一方法就是不要使用虛擬函數,但是這樣的話,函數就等于整個被替換掉了。首先,函數可能會在無意中被子類的函數給替換掉。在同一個scope中重新宣告一個函數將會導致名字沖突(name clash);編譯器將會就此報告出一個“duplicate declaration”的語法錯誤。允許兩個擁有同名的實體存在于同一個scope中將會導致語義的二義性(ambiguity)及其他問題(可參見于 name overloading這節)。
             
             下面的例子闡明了第二個問題:

             class A
             {
              public:
                   void nonvirt();
                   virtual void virt();
             };

             class B : public A
             {
              public:
                   void nonvirt();
                   void virt();
             };
             
             A a;
             B b;
             A *ap = &b;
             B *bp = &b;
             
             bp->nonvirt(); file://calls B::nonvirt as you would eXPect
             ap->nonvirt(); file://calls A::nonvirt even though this object is of type B
             ap->virt();  file://calls B::virt, the correct version of the routine for B objects

             
            在這個例子里,B擴展或替換掉了A中的函數。B::nonvirt是應該被B的對象調用的函數。在此處我們必須指出,C++給客戶端程序員(即使用我們這套繼承體系架構的程序員)足夠的彈性來調用A::nonvirt或是B::nonvirt,但我們也可以提供一種更簡單,更直接的方式:提供給A:: nonvirt和B::nonvirt不同的名字。這可以使得程序員能夠正確地,顯式地調用想要c調用的函數,而

            不是陷入了上面的那種晦澀的,容易導致錯誤的陷阱中去。

            具體方法如下:

             class B:  public A
             {
              public:
                   void b_nonvirt();
                   void virt();
             }

             B b;
             B *bp = &b;

            bp->nonvirt();  file://calls A::nonvirt
            bp->b_nonvirt(); file://calls B::b_nonvirt

             
              現在,B的設計者就可以直接的操縱B的接口了。程序要求B的客戶端(即調用B的代碼)能夠同時調用A::nonvirt和B::nonvirt,這點我們也做到了。就Object-Oriented Design(OOD)來說,這是一個不錯的做法,因為它提供了健壯的接口定義(strongly defined interface)【譯者認為:即不會引起調用歧義的接口】。C++允許客戶端程序員在類的接口處賣弄他們的技巧,借以對類進行擴展。在上例中所出現的就是設計B的程序員不能阻止其他程序員調用A::nonvirt。類B的對象擁有它們自己的nonvirt,但是即便如此,B的設計者也不能保證通過B的接口就一定能調用到正確版本的nonvirt。
             
             C++同樣不能阻止系統中對其他處的改動不會影響到B。假設我們需要寫一個類C,在C 中我們要求nonvirt是一個虛擬的函數。于是我們就必須回到A中將nonvirt改為虛擬的。但這又將使得我們對于B::nonvirt所玩弄的技巧又失去了作用(想想看,為什么:D)。對于C需要一個virtual的需求(將已有的nonvirtual改為virtual)使得我們改變了父類,這又使得所有從父類繼承下來的子類也相應地有了改變。這已經違背了OOP擁有低耦合的類的理由,新的需求,改動應該只產生局部的影響,而不是改變系統中其他地方,從而潛在地破壞了系統的已有部分。
             
             另一個問題是,同樣的一條語句必須一直保持著同樣的語義。例如:對于諸如a->f()這樣的多態性語句的解釋,系統調用的是由最符合a所真正指向類型的那個f(),而不管對象的類型到底是A,還是A的子類。然而,對于C++的程序員來說,他們必須要清楚地了解當f()被定義成virtual或是non-virtual時,a->f()的真正涵義。所以,語句a->f()不能獨立于其實現,而且隱藏的實現原理也不是一成不變的。對于f()聲明的一次改變將會相應地改變調用它時的語義。與實現獨立意味著對于實現的改變不會改變語句的語義,或是執行的語義。
             
             如果在聲明中的改變導致相應的語義改變,編譯器應該能檢測到錯誤的產生。程序員應該在聲明被改變的情況下保持語義的不變。這反映了軟件開發中的動態特性,在其中你將能發現程序文本的永久改變。
             
             其他另一個與a->f()相應的,語義不能被保持不變的例子是:構造函數(可參考于C++ ARM, section 10.9c, p 232)。而Eiffel和Java則不存在這樣的問題。它們中所采用的機制簡單而又清晰,不會導致C++中所產生的那些令人吃驚的現象。在Java中,所有的方法都是虛擬的,為了讓一個方法【譯者注:對應于C++的函數】不能被重寫,我們可以用final修飾符來修飾這個方法。
             
             Eiffel允許程序員指定一個函數為frozen,在這種情況下,這個函數就不能在子類中被重寫。
             
            選項2:

              是使用現有的函數還是重寫一個,這應該是由撰寫子類的程序員所決定的。在C++中,要想擁有這種能力則必須在父類中指定為virtual。對于OOD來說,你所決定不想作的與你所決定想作的同樣重要,你的決定應該是越遲下越好。這種策略可以避免錯誤在系統前期就被包含進去。你作決定越早,你就越有可能被以后所證明是錯誤的假設所包圍;或是你所作的假設在一種情況下是正確的,然而在另一種情況下卻會出錯,從而使得你所寫出來的軟件比較脆弱,不具有重用性(reusable)【譯者注:軟件的可重用性對于軟件來說是一個很重要的特性,具體可以參考
            《Object-Oriented Software Construct》中對于軟件的外部特性的敘述,P7, Reusability, Charpter 1.2 A REVIEW OF EXTERNAL FACTORS】。
             
             C ++要求我們在父類中就要指定可能的多態性(這可以通過virtual來指定),當然我們也可以在繼承鏈中的中間的類導入virtual機制,從而預先判斷某個函數是否可以在子類中被重定義。

            這種做法將導致問題的出現:如那些并非真正多態的函數(not actually polymorphic)也必須通過效率較低的table技術來被調用,而不像直接調用那個函數來的高效【譯者注:在文章的上下文中并沒有出現not actually polymorphic特性的確切定義,根據我的理解,應該是聲明為polymorphic,而實際上的動作并沒能體現polymorphic這樣的一種特性】。雖然這樣做并不會引起大量的花費(overhead),但我們知道,在OO程序中經常會出現使用大量的、短小的、目標單一明確的函數,如果將所有這些都累計下來,也會導致一個相當可觀的花費。C++中的

            政策是這樣的:需要被重定義的函數必須被聲明為virtual。糟糕的是,C++同時也說了, non-virtual函數不能被重定義,這使得設計使用子類的程序員就無法對于這些函數擁有自己的控制權。【譯者注:原作中此句顯得有待推敲,原文是這樣寫的:it says that non-virtual routines cannot be redefined, 我猜測作者想表達的意思應該是:If you have defined a non-virtual routine in base, then it cannot be virtual in the base whether you redefined it as virtual in descendant.】

            Rumbaugh等人對于C++中的虛擬機制的批評如下:C++擁有了簡單實現繼承及動態方法調用的特性,但一個C++的數據結構并不能自動成為面向對象的。方法調用決議(method resolution)以及在子類中重寫一個函數操作的前提必須是這個函數/方法已經在父類中被聲明為virtual。也就是說,必須在最初的類中我們就能預見到一個函數是否需要被重寫。不幸的是,類的撰寫者可能不會預期到需要定義一個特殊的子類,也可能不會知道那些操作將要在子類中被重寫。這意味著當子類被定義時,我們經常需要回過頭去修改我們的父類,并且使得對于通過創建子類來重用已有的庫的限制極為嚴格,尤其是當這個庫的源代碼不能被獲得是更是如此。(當然,你也可以將所有的操作都定義為virtual,并愿意為此付出一些小小的內存花費用于函數調用)【RBPEL91】
             
             然而,讓程序員來處理virtual是一個錯誤的機制。編譯器應該能夠檢測到多態,并為此產生所必須的、潛在的實現virtual的代碼。讓程序員來決定 virtual與否對于程序員來說是增加了一個簿記工作的負擔。這也就是為什么C++只能算是一種弱的面向對象語言(weak object-oriented language):因為程序員必須時刻注意著一些底層的細節(low level details),而這些本來可以由編譯器自動處理的。
             
             在C++中的另一個問題是錯誤的重寫(mistaken overriding),父類中的函數可以在毫不知情的情況下被重寫。編譯器應該對于同一個名字空間中的重定義報錯,除非編寫子類的程序員指出他是有意這么做的(即對于虛函數的重寫)。我們可以使用同一個名字,但是程序員必須清楚自己在干什么,并且顯式地聲明它,尤其是在將自己的程序與已經存在的程序組件組裝成新的系統的情況下更要如此。除非程序員顯式地重寫已有的虛函數,否則編譯器必須要給我們報告出現了名字被聲明多處(duplicate declaration)的錯誤。然而,C++卻采用了Simula最初的做法,而這種方法到現在已經得到了改良。其他的一些程序語言通過采用了更好的、更加顯式的方法,避免了錯誤重定義的出現。
             
             解決方法就是virtual不應該在父類中就被指定好。當我們需要運行時的動態綁定時,我們就在子類中指定需要對某個函數進行重寫。這樣做的好處在于:對于具有多態性的函數,編譯器可以檢測其函數簽名(function signature)的一致性;而對于重載的函數,其函數簽名在某些方面本來就不一樣。第二個好處表現在,在程序的維護階段,能夠清楚地表達程序的最初意愿。而實際上后來的程序員卻經常要猜測先前的程序員是不是犯了什么錯誤,選擇一個相同的名字,還是他本來就想重載這個函數。
             
             在 Java中,沒有virtual這個關鍵字,所有的方法在底層都是多態的。當方法被定義為static, private或是final時,Java直接調用它們而不是通過動態的查表的方式。這意味著在需要被動態調用時,它們卻是非多態性的函數,Java的這種動態特性使得編譯器難以進行進一步的優化。
             
             Eiffel和Object Pascal迎合了這個選項。在它們中,編寫子類的程序員必須指定他們所想進行的重定義動作。我們可以從這種做法中得到巨大的好處:對于以后將要閱讀這些程序的人及程序的將來維護者來說,可以很容易地找出來被重寫的函數。因而選項2最好是在子類中被實現。
             
             Eiffel和Object Pascal都優化了函數調用的方式:因為他們只需要產生那些真正多態的函數的調用分配表的入口項。對于怎樣做,我們將會在global analysis這節中討論。
             
            選項3:

              純虛函數這樣的做法迎合了讓一個函數成為抽象的,從而子類在實例化時必須為其提供一個實現這樣的一個條件。沒有重寫這些函數的任何子類同樣也是抽象類。這個概念沒有錯,但是請你看一看pure virtual functions這一節,我們將在那節中對于這種術語及語法進行批判討論。
             
             Java也擁有純虛方法(同樣Eiffel也有),實現方法是為該方法加上deffered標注。
             
            結論:

             virtual 的主要問題在于,它強迫編寫父類的程序員必須要猜測函數在子類中是否有多態性。如果這個需求沒有被預見到,或是為了優化、避免動態調用而沒有被包含進去的話,那么導致的可能性就是極大的封閉,勝過了開放。在C++的實現中,virtual提高了重寫的耦合性,導致了一種容易產生錯誤的聯合。

            Virtual是一種難以掌握的語法,相關的諸如多態、動態綁定、重定義以及重寫等概念由于面向于問題域本身,掌握起來就相對容易多了。虛擬函數的這種實現機制要求編譯器為其在class中建立起virtual table入口,而global analysis并不是由編譯器完成的,所以一切的重擔都壓在了程序員的肩上了。多態是目的,虛擬機制就是手段。Smalltalk, Objective-C, Java和Eiffel都是使用其他的一種不同的方法來實現多態的。
             
             Virtual是一個例子,展示了C ++在OOP的概念上的混沌不清。程序員必須了解一些底層的概念,甚至要超過了解那些高層次的面向對象的概念。Virtual把優化留給了程序員;其他的方法則是由編譯器來優化函數的動態調用,這樣做可以將那些不需要被動態調用的分配(即不需要在動態調用表中存在入口)100%地消除掉。對于底層機制,感興趣的應該是那些理論家及編譯器實現者,一般的從業者則沒有必要去理解它們,或是通過使用它們來搞清楚高層的概念。在實踐中不得不使用它們是一件單調乏味的事情,并且還容易導致出錯,這阻止了軟件在底層技術及運行機制下(參見并發程序)的更好適應,降低了軟件的彈性及可重用性。

            posted on 2007-09-27 02:51 lovedday 閱讀(713) 評論(4)  編輯 收藏 引用 所屬分類: ▲ C++ Program

            評論

            # re: 【ZT】C++批判(1) 2007-09-27 10:53 清源游民

            雖說是批判的文章,但從反面給C++學習者指導。兼聽則明,往往批判會比贊美對我們幫助更甚。從理論層面來討論,對我這樣知識比較單薄的人來說有點晦澀,幸好有例子,讓我可以更好理解作者意圖。趕緊發下一篇吧。  回復  更多評論   

            # re: 【ZT】C++批判(1) 2007-09-28 21:20 lovedday

            我覺得作者說的還是挺有道理的,不過我還得用C++,因為沒有更好的選擇,繼續回去使用C是不太可能的。  回復  更多評論   

            # re: 【ZT】C++批判(1) 2007-09-30 08:34 Minidx全文檢索

            選擇合適的語言就可以了吧,。
              回復  更多評論   

            # re: 【ZT】C++批判(1) 2008-12-02 14:00 momor

            文章不錯,用批駁的方式對問題發起思考,也是一種不錯的方式。
            但是對于某些論調,個人感覺不盡然,
            我認為:不要夢想虛函數作為你的救世主,它只是語言級別上對代碼版本的一種控制管理。我們知道,應該盡可能的避免在接口類寫實現代碼,同樣你也不應當將可變功能需求,妄想用虛函數一次性的解決,解決的方法很多,利用RTTI或者你自己實現的反射技術(不要被反射技術這個名詞嚇倒,其實自己在C++中實現一個很簡單),以組合類的方式功能添加到你的功能接口類中。同樣這也避免了過渡依賴繼承導致類的過渡縱向生長,使用組合多于繼承的原則  回復  更多評論   

            公告

            導航

            統計

            常用鏈接

            隨筆分類(178)

            3D游戲編程相關鏈接

            搜索

            最新評論

            久久久青草青青国产亚洲免观| 97久久久精品综合88久久| 久久丝袜精品中文字幕| 日韩中文久久| 99久久中文字幕| 久久无码AV中文出轨人妻| 亚洲αv久久久噜噜噜噜噜| 久久精品国产91久久麻豆自制 | 99久久精品国产一区二区蜜芽| 国产精品免费久久久久久久久| 一级女性全黄久久生活片免费| 91精品国产高清久久久久久io| 国产福利电影一区二区三区,免费久久久久久久精 | 97久久超碰国产精品旧版| 久久久久亚洲av毛片大| 久久99精品久久久久久hb无码| 久久国产视频99电影| 精品综合久久久久久888蜜芽| 久久国产影院| 人人狠狠综合久久亚洲婷婷| 精品一二三区久久aaa片| 久久精品无码一区二区app| 久久亚洲精品人成综合网| 欧美激情精品久久久久久| 色综合久久久久| 久久国产精品久久| 国产成人久久AV免费| 色88久久久久高潮综合影院| 免费精品国产日韩热久久| 久久精品免费网站网| 国产精久久一区二区三区| 伊人久久综在合线亚洲2019| 日韩AV无码久久一区二区| 7777精品久久久大香线蕉| 精品人妻伦九区久久AAA片69| 亚洲国产婷婷香蕉久久久久久| 久久成人国产精品一区二区| 久久99国产一区二区三区| 久久精品国产亚洲7777| 久久九色综合九色99伊人| 日韩va亚洲va欧美va久久|