• <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】

            該篇文章已經(jīng)包含在Ian Joyner所寫的《Objects Unencapsulated 》一書中(目前已經(jīng)有了日文的翻譯版本),該書的介紹可參見于:
            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


            虛擬函數(shù)

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

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

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

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

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

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

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

            如下所示,將輸出: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;
            }


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

             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中的函數(shù)。B::nonvirt是應(yīng)該被B的對象調(diào)用的函數(shù)。在此處我們必須指出,C++給客戶端程序員(即使用我們這套繼承體系架構(gòu)的程序員)足夠的彈性來調(diào)用A::nonvirt或是B::nonvirt,但我們也可以提供一種更簡單,更直接的方式:提供給A:: nonvirt和B::nonvirt不同的名字。這可以使得程序員能夠正確地,顯式地調(diào)用想要c調(diào)用的函數(shù),而

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

            具體方法如下:

             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

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

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

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

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

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

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

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

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

            評論

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

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

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

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

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

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

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

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

            公告

            導(dǎo)航

            統(tǒng)計

            常用鏈接

            隨筆分類(178)

            3D游戲編程相關(guān)鏈接

            搜索

            最新評論

            国产成人无码精品久久久性色| 欧洲性大片xxxxx久久久| 亚洲综合伊人久久大杳蕉| 亚洲AV无码久久精品蜜桃| 久久久久久久久久久久中文字幕 | 久久久久久精品久久久久| 人妻无码αv中文字幕久久琪琪布 人妻无码精品久久亚瑟影视 | 99久久做夜夜爱天天做精品| 欧美噜噜久久久XXX| 久久久久久av无码免费看大片| 亚洲午夜久久久久久噜噜噜| 办公室久久精品| 国内精品久久久久久99| 久久毛片免费看一区二区三区| 日产精品99久久久久久| 久久久久久一区国产精品| 亚洲色大成网站WWW久久九九| 品成人欧美大片久久国产欧美...| 久久精品国产亚洲AV不卡| 久久亚洲精品无码播放| 久久99毛片免费观看不卡| 日本久久久久亚洲中字幕| 色综合久久久久综合99| 久久99热精品| 青青草国产精品久久| AV狠狠色丁香婷婷综合久久| 久久久国产99久久国产一| 久久无码AV中文出轨人妻| 久久精品国产99国产电影网 | 天堂久久天堂AV色综合| 无码国内精品久久综合88| 亚洲欧美日韩精品久久亚洲区| 国产高清美女一级a毛片久久w| 久久91精品久久91综合| 久久精品国产99国产电影网| 天堂久久天堂AV色综合 | 94久久国产乱子伦精品免费 | 日本五月天婷久久网站| 中文字幕无码av激情不卡久久| 很黄很污的网站久久mimi色| 狠狠色综合网站久久久久久久|