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

            陳碩的Blog

            C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice

             

            摘要:作為 C++ 動(dòng)態(tài)庫(kù)的作者,應(yīng)當(dāng)避免使用虛函數(shù)作為庫(kù)的接口。這么做會(huì)給保持二進(jìn)制兼容性帶來很大麻煩,不得不增加很多不必要的 interfaces,最終重蹈 COM 的覆轍。

            本文主要討論 Linux x86 平臺(tái),會(huì)繼續(xù)舉 Windows/COM 作為反面教材。

            本文是上一篇《C++ 工程實(shí)踐(4):二進(jìn)制兼容性》的延續(xù),在寫這篇文章的時(shí)候,我原本以外大家都對(duì)“以 C++ 虛函數(shù)作為接口”的害處達(dá)成共識(shí),我就寫得比較簡(jiǎn)略,看來情況不是這樣,我還得展開談一談。

            “接口”有廣義和狹義之分,本文用中文“接口”表示廣義的接口,即一個(gè)庫(kù)的代碼界面;用英文 interface 表示狹義的接口,即只包含 virtual function 的 class,這種 class 通常沒有 data member,在 Java 里有一個(gè)專門的關(guān)鍵字 interface 來表示它。

            C++ 程序庫(kù)的作者的生存環(huán)境

            假設(shè)你是一個(gè) shared library 的維護(hù)者,你的 library 被公司另外兩三個(gè)團(tuán)隊(duì)使用了。你發(fā)現(xiàn)了一個(gè)安全漏洞,或者某個(gè)會(huì)導(dǎo)致 crash 的 bug 需要緊急修復(fù),那么你修復(fù)之后,能不能直接部署 library 的二進(jìn)制文件?有沒有破壞二進(jìn)制兼容性?會(huì)不會(huì)破壞別人團(tuán)隊(duì)已經(jīng)編譯好的投入生成環(huán)境的可執(zhí)行文件?是不是要強(qiáng)迫別的團(tuán)隊(duì)重新編譯鏈接,把可執(zhí)行文件也發(fā)布新版本?會(huì)不會(huì)打亂別人的 release cycle?這些都是工程開發(fā)中經(jīng)常要遇到的問題。

            如果你打算新寫一個(gè) C++ library,那么通常要做以下幾個(gè)決策:

            • 以什么方式發(fā)布?動(dòng)態(tài)庫(kù)還是靜態(tài)庫(kù)?(本文不考慮源代碼發(fā)布這種情況,這其實(shí)和靜態(tài)庫(kù)類似。)
            • 以什么方式暴露庫(kù)的接口?可選的做法有:以全局(含 namespace 級(jí)別)函數(shù)為接口、以 class 的 non-virtual 成員函數(shù)為接口、以 virtual 函數(shù)為接口(interface)。

            (Java 程序員沒有這么多需要考慮的,直接寫 class 成員函數(shù)就行,最多考慮一下要不要給 method 或 class 標(biāo)上 final。也不必考慮動(dòng)態(tài)庫(kù)靜態(tài)庫(kù),都是 .jar 文件。)

            在作出上面兩個(gè)決策之前,我們考慮兩個(gè)基本假設(shè):

            • 代碼會(huì)有 bug,庫(kù)也不例外。將來可能會(huì)發(fā)布 bug fixes。
            • 會(huì)有新的功能需求。寫代碼不是一錘子買賣,總是會(huì)有新的需求冒出來,需要程序員往庫(kù)里增加?xùn)|西。這是好事情,讓程序員不丟飯碗。

            (如果你的代碼第一次發(fā)布的時(shí)候就已經(jīng)做到完美,將來不需要任何修改,那么怎么做都行,也就不必繼續(xù)閱讀本文。)

            也就是說,在設(shè)計(jì)庫(kù)的時(shí)候必須要考慮將來如何升級(jí)

            基于以上兩個(gè)基本假設(shè)來做決定。第一個(gè)決定很好做,如果需要 hot fix,那么只能用動(dòng)態(tài)庫(kù);否則,在分布式系統(tǒng)中使用靜態(tài)庫(kù)更容易部署,這在前文中已經(jīng)談過。(“動(dòng)態(tài)庫(kù)比靜態(tài)庫(kù)節(jié)約內(nèi)存”這種優(yōu)勢(shì)在今天看來已不太重要。)

            以下本文假定你或者你的老板選擇以動(dòng)態(tài)庫(kù)方式發(fā)布,即發(fā)布 .so 或 .dll 文件,來看看第二個(gè)決定怎么做。(再說一句,如果你能夠以靜態(tài)庫(kù)方式發(fā)布,后面的麻煩都不會(huì)遇到。)

            第二個(gè)決定不是那么容易做,關(guān)鍵問題是,要選擇一種可擴(kuò)展的 (extensible) 接口風(fēng)格,讓庫(kù)的升級(jí)變得更輕松。“升級(jí)”有兩層意思:

            • 對(duì)于 bug fix only 的升級(jí),二進(jìn)制庫(kù)文件的替換應(yīng)該兼容現(xiàn)有的二進(jìn)制可執(zhí)行文件,二進(jìn)制兼容性方面的問題已經(jīng)在前文談過,這里從略。
            • 對(duì)于新增功能的升級(jí),應(yīng)該對(duì)客戶代碼的友好。升級(jí)庫(kù)之后,客戶端使用新功能的代價(jià)應(yīng)該比較小。只需要包含新的頭文件(這一步都可以省略,如果新功能已經(jīng)加入原有的頭文件中),然后編寫新代碼即可。而且,不要在客戶代碼中留下垃圾,后文我們會(huì)談到什么是垃圾。

            在討論虛函數(shù)接口的弊端之前,我們先看看虛函數(shù)做接口的常見用法。

            虛函數(shù)作為庫(kù)的接口的兩大用途

            虛函數(shù)為接口大致有這么兩種用法:

            1. 調(diào)用,也就是庫(kù)提供一個(gè)什么功能(比如繪圖 Graphics),以虛函數(shù)為接口方式暴露給客戶端代碼。客戶端代碼一般不需要繼承這個(gè) interface,而是直接調(diào)用其 member function。這么做據(jù)說是有利于接口和實(shí)現(xiàn)分離,我認(rèn)為純屬脫了褲子放屁。
            2. 回調(diào),也就是事件通知,比如網(wǎng)絡(luò)庫(kù)的“連接建立”、“數(shù)據(jù)到達(dá)”、“連接斷開”等等。客戶端代碼一般會(huì)繼承這個(gè) interface,然后把對(duì)象實(shí)例注冊(cè)到庫(kù)里邊,等庫(kù)來回調(diào)自己。一般來說客戶端不會(huì)自己去調(diào)用這些 member function,除非是為了寫單元測(cè)試模擬庫(kù)的行為。
            3. 混合,一個(gè) class 既可以被客戶端代碼繼承用作回調(diào),又可以被客戶端直接調(diào)用。說實(shí)話我沒看出這么做的好處,但實(shí)際中某些面向?qū)ο蟮?C++ 庫(kù)就是這么設(shè)計(jì)的。

            對(duì)于“回調(diào)”方式,現(xiàn)代 C++ 有更好的做法,即 boost::function + boost::bind,見參考文獻(xiàn)[4],muduo 的回調(diào)全部采用這種新方法,見《Muduo 網(wǎng)絡(luò)編程示例之零:前言》。本文以下不考慮以虛函數(shù)為回調(diào)的過時(shí)做法。

            對(duì)于“調(diào)用”方式,這里舉一個(gè)虛構(gòu)的圖形庫(kù),這個(gè)庫(kù)的功能是畫線、畫矩形、畫圓弧:

               1: struct Point
               2: {
               3:   int x;
               4:   int y;
               5: };
               6:  
               7: class Graphics
               8: {
               9:   virtual void drawLine(int x0, int y0, int x1, int y1);
              10:   virtual void drawLine(Point p0, Point p1);
              11:  
              12:   virtual void drawRectangle(int x0, int y0, int x1, int y1);
              13:   virtual void drawRectangle(Point p0, Point p1);
              14:  
              15:   virtual void drawArc(int x, int y, int r);
              16:   virtual void drawArc(Point p, int r);
              17: };

            這里略去了很多與本文主題無關(guān)細(xì)節(jié),比如 Graphics 的構(gòu)造與析構(gòu)、draw*() 函數(shù)應(yīng)該是 public、Graphics 應(yīng)該不允許復(fù)制,還比如 Graphics 可能會(huì)用 pure virtual functions 等等,這些都不影響本文的討論。

            這個(gè) Graphics 庫(kù)的使用很簡(jiǎn)單,客戶端看起來是這個(gè)樣子。

            Graphics* g = getGraphics();

            g->drawLine(0, 0, 100, 200);

            releaseGraphics(g); g = NULL;

            似乎一切都很好,陽光明媚,符合“面向?qū)ο蟮脑瓌t”,但是一旦考慮升級(jí),前景立刻變得昏暗。

            虛函數(shù)作為接口的弊端

            以虛函數(shù)作為接口在二進(jìn)制兼容性方面有本質(zhì)困難:“一旦發(fā)布,不能修改”。

            假如我需要給 Graphics 增加幾個(gè)繪圖函數(shù),同時(shí)保持二進(jìn)制兼容性。這幾個(gè)新函數(shù)的坐標(biāo)以浮點(diǎn)數(shù)表示,我理想中的新接口是:

            --- old/graphics.h  2011-03-12 13:12:44.000000000 +0800
            +++ new/graphics.h 2011-03-12 13:13:30.000000000 +0800
            @@ -7,11 +7,14 @@
             class Graphics
             {
               virtual void drawLine(int x0, int y0, int x1, int y1);
            +  virtual void drawLine(double x0, double y0, double x1, double y1);
               virtual void drawLine(Point p0, Point p1);
            
               virtual void drawRectangle(int x0, int y0, int x1, int y1);
            +  virtual void drawRectangle(double x0, double y0, double x1, double y1);
               virtual void drawRectangle(Point p0, Point p1);
            
               virtual void drawArc(int x, int y, int r);
            +  virtual void drawArc(double x, double y, double r);
               virtual void drawArc(Point p, int r);
             };

            受 C++ 二進(jìn)制兼容性方面的限制,我們不能這么做。其本質(zhì)問題在于 C++ 以 vtable[offset] 方式實(shí)現(xiàn)虛函數(shù)調(diào)用,而 offset 又是根據(jù)虛函數(shù)聲明的位置隱式確定的,這造成了脆弱性。我增加了 drawLine(double x0, double y0, double x1, double y1),造成 vtable 的排列發(fā)生了變化,現(xiàn)有的二進(jìn)制可執(zhí)行文件無法再用舊的 offset 調(diào)用到正確的函數(shù)。

            怎么辦呢?有一種危險(xiǎn)且丑陋的做法:把新的虛函數(shù)放到 interface 的末尾,例如:

            --- old/graphics.h  2011-03-12 13:12:44.000000000 +0800
            +++ new/graphics.h 2011-03-12 13:58:22.000000000 +0800
            @@ -7,11 +7,15 @@
             class Graphics
             {
               virtual void drawLine(int x0, int y0, int x1, int y1);
               virtual void drawLine(Point p0, Point p1);
            
               virtual void drawRectangle(int x0, int y0, int x1, int y1);
               virtual void drawRectangle(Point p0, Point p1);
            
               virtual void drawArc(int x, int y, int r);
               virtual void drawArc(Point p, int r);
            +
            +  virtual void drawLine(double x0, double y0, double x1, double y1);
            +  virtual void drawRectangle(double x0, double y0, double x1, double y1);
            +  virtual void drawArc(double x, double y, double r);
             };

            這么做很丑陋,因?yàn)樾碌?drawLine(double x0, double y0, double x1, double y1) 函數(shù)沒有和原來的 drawLine() 函數(shù)呆在一起,造成閱讀上的不便。這么做同時(shí)很危險(xiǎn),因?yàn)?Graphics 如果被繼承,那么新增虛函數(shù)會(huì)改變派生類中的 vtable offset 變化,同樣不是二進(jìn)制兼容的。

            另外有兩種似乎安全的做法,這也是 COM 采用的辦法:

            1. 通過鏈?zhǔn)嚼^承來擴(kuò)展現(xiàn)有 interface,例如從 Graphics 派生出 Graphics2。

            --- graphics.h  2011-03-12 13:12:44.000000000 +0800
            +++ graphics2.h 2011-03-12 13:58:35.000000000 +0800
            @@ -7,11 +7,19 @@
             class Graphics
             {
               virtual void drawLine(int x0, int y0, int x1, int y1);
               virtual void drawLine(Point p0, Point p1);
            
               virtual void drawRectangle(int x0, int y0, int x1, int y1);
               virtual void drawRectangle(Point p0, Point p1);
            
               virtual void drawArc(int x, int y, int r);
               virtual void drawArc(Point p, int r);
             };
            +
            +class Graphics2 : public Graphics
            +{
            +  using Graphics::drawLine;
            +  using Graphics::drawRectangle;
            +  using Graphics::drawArc;
            +
            +  // added in version 2
            +  virtual void drawLine(double x0, double y0, double x1, double y1);
            +  virtual void drawRectangle(double x0, double y0, double x1, double y1);
            +  virtual void drawArc(double x, double y, double r);
            +};

            將來如果繼續(xù)增加功能,那么還會(huì)有 class Graphics3 : public Graphics2;以及 class Graphics4 : public Graphics3 等等。這么做和前面的做法一樣丑陋,因?yàn)樾碌?drawLine(double x0, double y0, double x1, double y1) 函數(shù)位于派生 Graphics2 interace 中,沒有和原來的 drawLine() 函數(shù)呆在一起,造成割裂。

            2. 通過多重繼承來擴(kuò)展現(xiàn)有 interface,例如定義一個(gè)與 Graphics class 有同樣成員的 Graphics2,再讓實(shí)現(xiàn)同時(shí)繼承這兩個(gè) interfaces。

            --- graphics.h  2011-03-12 13:12:44.000000000 +0800
            +++ graphics2.h 2011-03-12 13:16:45.000000000 +0800
            @@ -7,11 +7,32 @@
             class Graphics
             {
               virtual void drawLine(int x0, int y0, int x1, int y1);
               virtual void drawLine(Point p0, Point p1);
            
               virtual void drawRectangle(int x0, int y0, int x1, int y1);
               virtual void drawRectangle(Point p0, Point p1);
            
               virtual void drawArc(int x, int y, int r);
               virtual void drawArc(Point p, int r);
             };
            +
            +class Graphics2
            +{
            +  virtual void drawLine(int x0, int y0, int x1, int y1);
            +  virtual void drawLine(double x0, double y0, double x1, double y1);
            +  virtual void drawLine(Point p0, Point p1);
            +
            +  virtual void drawRectangle(int x0, int y0, int x1, int y1);
            +  virtual void drawRectangle(double x0, double y0, double x1, double y1);
            +  virtual void drawRectangle(Point p0, Point p1);
            +
            +  virtual void drawArc(int x, int y, int r);
            +  virtual void drawArc(double x, double y, double r);
            +  virtual void drawArc(Point p, int r);
            +};
            +
            +// 在實(shí)現(xiàn)中采用多重接口繼承
            +class GraphicsImpl : public Graphics,  // version 1
            +                     public Graphics2, // version 2
            +{
            +  // ...
            +};

            這種帶版本的 interface 的做法在 COM 使用者的眼中看起來是很正常的,解決了二進(jìn)制兼容性的問題,客戶端源代碼也不受影響。

            在我看來帶版本的 interface 實(shí)在是很丑陋,因?yàn)槊看胃膭?dòng)都引入了新的 interface class,會(huì)造成日后客戶端代碼難以管理。比如,如果代碼使用了 Graphics3 的功能,要不要把現(xiàn)有的 Graphics2 都替換掉?

            • 如果不替換,一個(gè)程序同時(shí)依賴多個(gè)版本的 Graphics,一直背著歷史包袱。依賴的 Graphics 版本越積越多,將來如何管理得過來?
            • 如果要替換,為什么不相干的代碼(現(xiàn)有的運(yùn)行得好好的使用 Graphics2 的代碼)也會(huì)因?yàn)閯e處用到了 Graphics3 而被修改?

            這種二難境地純粹是“以虛函數(shù)為庫(kù)的接口”造成的。如果我們能直接原地?cái)U(kuò)充 class Graphics,就不會(huì)有這些屁事,見本文“推薦做法”一節(jié)。

            假如 Linux 系統(tǒng)調(diào)用以 COM 接口方式實(shí)現(xiàn)

            或許上面這個(gè) Graphics 的例子太簡(jiǎn)單,沒有讓“以虛函數(shù)為接口”的缺點(diǎn)充分暴露出來,讓我們看一個(gè)真實(shí)的案例:Linux Kernel。

            Linux kernel 從 0.10 的 67 個(gè)系統(tǒng)調(diào)用發(fā)展到 2.6.37 的 340 個(gè),kernel interface 一直在擴(kuò)充,而且保持良好的兼容性,它保持兼容性的辦法很土,就是給每個(gè) system call 賦予一個(gè)終身不變的數(shù)字代號(hào),等于把虛函數(shù)表的排列固定下來。點(diǎn)開本段開頭的兩個(gè)鏈接,你就能看到 fork() 在 Linux 0.10 和 Linux 2.6.37 里的代號(hào)都是 2。(系統(tǒng)調(diào)用的編號(hào)跟硬件平臺(tái)有關(guān),這里我們看的是 x86 32-bit 平臺(tái)。)

            試想假如 Linus 當(dāng)初選擇用 COM 接口的鏈?zhǔn)嚼^承風(fēng)格來描述,將會(huì)是怎樣一種壯觀的景象?為了避免擾亂視線,請(qǐng)移步觀看近百層繼承的代碼。(先后關(guān)系與版本號(hào)不一定 100% 準(zhǔn)確,我是用 git blame 去查的,現(xiàn)在列出的代碼只從 0.01 到 2.5.31,相信已經(jīng)足以展現(xiàn) COM 接口方式的弊端。)

             

            不要誤認(rèn)為“接口一旦發(fā)布就不能更改”是天經(jīng)地義的,那不過是“以 C++ 虛函數(shù)為接口”的固有弊端,如果跳出這個(gè)框框去思考,其實(shí) C++ 庫(kù)的接口很容易做得更好。

            為什么不能改?還不是因?yàn)橛昧薈++ 虛函數(shù)作為接口。Java 的 interface 可以添加新函數(shù),C 語言的庫(kù)也可以添加新的全局函數(shù),C++ class 也可以添加新 non-virtual 成員函數(shù)和 namespace 級(jí)別的 non-member 函數(shù),這些都不需要繼承出新 interface 就能擴(kuò)充原有接口。偏偏 COM 的 interface 不能原地?cái)U(kuò)充,只能通過繼承來 workaround,產(chǎn)生一堆帶版本的 interfaces。有人說 COM 是二進(jìn)制兼容性的正面例子,某深不以為然。COM 確實(shí)以一種最丑陋的方式做到了“二進(jìn)制兼容”。脆弱與僵硬就是以 C++ 虛函數(shù)為接口的宿命。

            相反,Linux 系統(tǒng)調(diào)用以編譯期常數(shù)方式固定下來,萬年不變,輕而易舉地解決了這個(gè)問題。在其他面向?qū)ο笳Z言(Java/C#)中,我也沒有見過每改動(dòng)一次就給 interface 遞增版本號(hào)的詭異做法。

            還是應(yīng)了《The Zen of Python》中的那句話,Explicit is better than implicit, Flat is better than nested.

             

            動(dòng)態(tài)庫(kù)的接口的推薦做法

            取決于動(dòng)態(tài)庫(kù)的使用范圍,有兩類做法。

            如果,動(dòng)態(tài)庫(kù)的使用范圍比較窄,比如本團(tuán)隊(duì)內(nèi)部的兩三個(gè)程序在用,用戶都是受控的,要發(fā)布新版本也比較容易協(xié)調(diào),那么不用太費(fèi)事,只要做好發(fā)布的版本管理就行了。再在可執(zhí)行文件中使用 rpath 把庫(kù)的完整路徑確定下來。

            比如現(xiàn)在 Graphics 庫(kù)發(fā)布了 1.1.0 和 1.2.0 兩個(gè)版本,這兩個(gè)版本可以不必是二進(jìn)制兼容。用戶的代碼從 1.1.0 升級(jí)到 1.2.0 的時(shí)候要重新編譯一下,反正他們要用新功能都是要重新編譯代碼的。如果要原地打補(bǔ)丁,那么 1.1.1 應(yīng)該和 1.1.0 二進(jìn)制兼容,而 1.2.1 應(yīng)該和 1.2.0 兼容。如果要加入新的功能,而新的功能與 1.2.0 不兼容,那么應(yīng)該發(fā)布到 1.3.0 版本。

            為了便于檢查二進(jìn)制兼容性,可考慮把庫(kù)的代碼的暴露情況分辨清楚。muduo 的頭文件和 class 就有意識(shí)地分為用戶可見和用戶不可見兩部分,見 http://blog.csdn.net/Solstice/archive/2010/08/29/5848547.aspx#_Toc32039。對(duì)于用戶可見的部分,升級(jí)時(shí)要注意二進(jìn)制兼容性,選用合理的版本號(hào);對(duì)于用戶不可見的部分,在升級(jí)庫(kù)的時(shí)候就不必在意。另外 muduo 本身設(shè)計(jì)來是以靜態(tài)庫(kù)方式發(fā)布,在二進(jìn)制兼容性方面沒有做太多的考慮。

             

            如果庫(kù)的使用范圍很廣,用戶很多,各家的 release cycle 不盡相同,那么推薦 pimpl 技法[2, item 43],并考慮多采用 non-member non-friend function in namespace [1, item 23] [2, item 44 abd 57] 作為接口。這里以前面的 Graphics 為例,說明 pimpl 的基本手法。

            1. 暴露的接口里邊不要有虛函數(shù),而且 sizeof(Graphics) == sizeof(Graphics::Impl*)。

            class Graphics
            {
             public:
              Graphics(); // outline ctor
              ~Graphics(); // outline dtor
            
              void drawLine(int x0, int y0, int x1, int y1);
              void drawLine(Point p0, Point p1);
            
              void drawRectangle(int x0, int y0, int x1, int y1);
              void drawRectangle(Point p0, Point p1);
            
              void drawArc(int x, int y, int r);
              void drawArc(Point p, int r);
            
             private:
              class Impl;
              boost::scoped_ptr<Impl> impl;
            };

            2. 在庫(kù)的實(shí)現(xiàn)中把調(diào)用轉(zhuǎn)發(fā) (forward) 給實(shí)現(xiàn) Graphics::Impl ,這部分代碼位于 .so/.dll 中,隨庫(kù)的升級(jí)一起變化。

            #include <graphics.h>
            
            class Graphics::Impl
            {
             public:
              void drawLine(int x0, int y0, int x1, int y1);
              void drawLine(Point p0, Point p1);
            
              void drawRectangle(int x0, int y0, int x1, int y1);
              void drawRectangle(Point p0, Point p1);
            
              void drawArc(int x, int y, int r);
              void drawArc(Point p, int r);
            };
            
            Graphics::Graphics()
              : impl(new Impl)
            {
            }
            
            Graphics::~Graphics()
            {
            }
            
            void Graphics::drawLine(int x0, int y0, int x1, int y1)
            {
              impl->drawLine(x0, y0, x1, y1);
            }
            
            void Graphics::drawLine(Point p0, Point p1)
            {
              impl->drawLine(p0, p1);
            }
            
            // ...

            3. 如果要加入新的功能,不必通過繼承來擴(kuò)展,可以原地修改,且很容易保持二進(jìn)制兼容性。先動(dòng)頭文件:

            --- old/graphics.h     2011-03-12 15:34:06.000000000 +0800
            +++ new/graphics.h    2011-03-12 15:14:12.000000000 +0800
            @@ -7,19 +7,22 @@
             class Graphics
             {
              public:
               Graphics(); // outline ctor
               ~Graphics(); // outline dtor
            
               void drawLine(int x0, int y0, int x1, int y1);
            +  void drawLine(double x0, double y0, double x1, double y1);
               void drawLine(Point p0, Point p1);
            
               void drawRectangle(int x0, int y0, int x1, int y1);
            +  void drawRectangle(double x0, double y0, double x1, double y1);
               void drawRectangle(Point p0, Point p1);
            
               void drawArc(int x, int y, int r);
            +  void drawArc(double x, double y, double r);
               void drawArc(Point p, int r);
            
              private:
               class Impl;
               boost::scoped_ptr<Impl> impl;
             };

            然后在實(shí)現(xiàn)文件里增加 forward,這么做不會(huì)破壞二進(jìn)制兼容性,因?yàn)樵黾?non-virtual 函數(shù)不影響現(xiàn)有的可執(zhí)行文件。

            --- old/graphics.cc    2011-03-12 15:15:20.000000000 +0800
            +++ new/graphics.cc   2011-03-12 15:15:26.000000000 +0800
            @@ -1,35 +1,43 @@
             #include <graphics.h>
            
             class Graphics::Impl
             {
              public:
               void drawLine(int x0, int y0, int x1, int y1);
            +  void drawLine(double x0, double y0, double x1, double y1);
               void drawLine(Point p0, Point p1);
            
               void drawRectangle(int x0, int y0, int x1, int y1);
            +  void drawRectangle(double x0, double y0, double x1, double y1);
               void drawRectangle(Point p0, Point p1);
            
               void drawArc(int x, int y, int r);
            +  void drawArc(double x, double y, double r);
               void drawArc(Point p, int r);
             };
            
             Graphics::Graphics()
               : impl(new Impl)
             {
             }
            
             Graphics::~Graphics()
             {
             }
            
             void Graphics::drawLine(int x0, int y0, int x1, int y1)
             {
               impl->drawLine(x0, y0, x1, y1);
             }
            
            +void Graphics::drawLine(double x0, double y0, double x1, double y1)
            +{
            +  impl->drawLine(x0, y0, x1, y1);
            +}
            +
             void Graphics::drawLine(Point p0, Point p1)
             {
               impl->drawLine(p0, p1);
             }

            采用 pimpl 多了一道 explicit forward 的手續(xù),帶來的好處是可擴(kuò)展性與二進(jìn)制兼容性,通常是劃算的。pimpl 扮演了編譯器防火墻的作用。

            pimpl 不僅 C++ 語言可以用,C 語言的庫(kù)同樣可以用,一樣帶來二進(jìn)制兼容性的好處,比如 libevent2 里邊的 struct event_base 是個(gè) opaque pointer,客戶端看不到其成員,都是通過 libevent 的函數(shù)和它打交道,這樣庫(kù)的版本升級(jí)比較容易做到二進(jìn)制兼容。

             

            為什么 non-virtual 函數(shù)比 virtual 函數(shù)更健壯?因?yàn)?virtual function 是 bind-by-vtable-offset,而 non-virtual function 是 bind-by-name。加載器 (loader) 會(huì)在程序啟動(dòng)時(shí)做決議(resolution),通過 mangled name 把可執(zhí)行文件和動(dòng)態(tài)庫(kù)鏈接到一起。就像使用 Internet 域名比使用 IP 地址更能適應(yīng)變化一樣。

             

            萬一要跨語言怎么辦?很簡(jiǎn)單,暴露 C 語言的接口。Java 有 JNI 可以調(diào)用 C 語言的代碼;Python/Perl/Ruby 等等的解釋器都是 C 語言編寫的,使用 C 函數(shù)也不在話下。C 函數(shù)是 Linux 下的萬能接口。

            本文只談了使用 class 為接口,其實(shí)用 free function 有時(shí)候更好(比如 muduo/base/Timestamp.h 除了定義 class Timestamp,還定義了 muduo::timeDifference() 等 free function),這也是 C++ 比 Java 等純面向?qū)ο笳Z言優(yōu)越的地方。留給將來再細(xì)談吧。

            參考文獻(xiàn)

            [1] Scott Meyers, 《Effective C++》 第 3 版,條款 35:考慮 virtual 函數(shù)以外的其他選擇;條款 23:寧以 non-member、non-friend 替換 member 函數(shù)

            [2] Herb Sutter and Andrei Alexandrescu, 《C++ 編程規(guī)范》,條款 39:考慮將 virtual 函數(shù)做成 non-public,將 public 函數(shù)做成 non-virtual;條款 43:明智地使用 pimpl;條款 44:盡可能編寫 nonmember, nonfriend 函數(shù);條款 57:將 class 和其非成員函數(shù)接口放入同一個(gè) namespace

            [3] 孟巖,《function/bind的救贖(上)》,《回復(fù)幾個(gè)問題》中的“四個(gè)半抽象”。

            [4] 陳碩,《以 boost::function 和 boost:bind 取代虛函數(shù)》,《樸實(shí)的 C++ 設(shè)計(jì)》。

            知識(shí)共享許可協(xié)議
            作品采用知識(shí)共享署名-非商業(yè)性使用-相同方式共享 3.0 Unported許可協(xié)議進(jìn)行許可。

            posted on 2011-03-13 09:04 陳碩 閱讀(12236) 評(píng)論(8)  編輯 收藏 引用

            評(píng)論

            # re: C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口 2011-03-13 15:26 yrj

            vtable[offset] 和系統(tǒng)調(diào)用有本質(zhì)的不同嗎?  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口 2011-03-13 16:59 陳梓瀚(vczh)

            其實(shí)大多數(shù)問題都?xì)w結(jié)于:人們不肯使用單一版本的編譯器編譯軟件所依賴的所有庫(kù)

            偷懶1:懶得在修改完【release的時(shí)候】編譯所有代碼
            偷懶2:在編譯器升級(jí)之后懶得編譯所有代碼

            何苦呢,調(diào)試就無所謂了,反正release嘛就都用相同的東西編譯了好了,現(xiàn)在網(wǎng)絡(luò)帶寬那么大,就算你要升級(jí),幾個(gè)dll的尺寸算毛。

            除非,你用的庫(kù)沒源代碼,那就算了,庫(kù)的作者們都會(huì)extern "C"的。  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口 2011-03-14 08:45 陳碩

            @yrj
            請(qǐng)先定義“本質(zhì)”,explicit 與 implicit 算不算本質(zhì)?  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口 2011-03-14 09:00 陳碩

            @陳梓瀚(vczh)
            1. 我的這兩篇文章跟“編譯器版本”或“編譯器升級(jí)”有任何關(guān)系嗎?所有庫(kù)和可執(zhí)行文件都應(yīng)該用相同的編譯器版本來 build,不影響文章的觀點(diǎn)。
            2. 這兩篇文章主要是從庫(kù)的作者的角度來談,庫(kù)的作者和應(yīng)用程序的作者是兩個(gè)人群。如果要求每個(gè)應(yīng)用程序都自己編譯所用的動(dòng)態(tài)庫(kù),那么這屬于我說的“源代碼發(fā)布”,和靜態(tài)發(fā)布是一樣的,也不用怎么考慮二進(jìn)制兼容性,只要應(yīng)用程序做好 full build 就行。
            3. extern "C" 跟本文有什么關(guān)系?動(dòng)態(tài)庫(kù)必須以 extern "C" 來提供接口?  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口 2011-03-14 13:13 yrj

            @陳碩
            我覺得 vtable[offset] 的 offset 和系統(tǒng)調(diào)用編號(hào)是一樣的,當(dāng)然它們的實(shí)現(xiàn)是不同的。增加新功能要保持二進(jìn)制兼容,offset 或系統(tǒng)調(diào)用編號(hào)都不能變。  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口 2011-03-16 17:56 shantai

            非常認(rèn)同博主的觀點(diǎn),尤其是庫(kù)的作者和使用者是不同的人群的時(shí)候,非常好。  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口[未登錄] 2011-03-26 17:23 sarrow

            我兩三年前有了你這些想法的雛形——但是我不敢罵com。討論得也沒有你這么深入。

            mark  回復(fù)  更多評(píng)論   

            # re: C++ 工程實(shí)踐(5):避免使用虛函數(shù)作為庫(kù)的接口[未登錄] 2011-04-14 12:24 cc

            DO(cmd, inparams, ouparams); 任何庫(kù)都要且緊要這一個(gè)函數(shù)作為接口。是這意思么?
              回復(fù)  更多評(píng)論   


            只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。
            網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


            <2011年3月>
            272812345
            6789101112
            13141516171819
            20212223242526
            272829303112
            3456789

            導(dǎo)航

            統(tǒng)計(jì)

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊(cè)

            搜索

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            伊人久久大香线蕉亚洲| 精品乱码久久久久久久| 国产精品免费久久久久电影网| 久久免费小视频| 欧美亚洲日本久久精品| 精品国产乱码久久久久软件| 99久久国产综合精品女同图片 | 久久se这里只有精品| 久久精品国产亚洲av瑜伽| 91麻豆国产精品91久久久| 无码AV波多野结衣久久| 色综合久久中文综合网| 久久久午夜精品| 一本大道久久a久久精品综合| 欧美午夜精品久久久久久浪潮| 久久久久亚洲Av无码专| 狠狠色伊人久久精品综合网| 亚洲日韩中文无码久久| 国产福利电影一区二区三区久久久久成人精品综合 | 亚洲va久久久噜噜噜久久天堂| 久久免费美女视频| 少妇久久久久久被弄高潮| 久久国产香蕉一区精品| 国产成人久久AV免费| 亚洲国产成人精品无码久久久久久综合| 无码国内精品久久人妻蜜桃| 久久精品国产99久久丝袜| 国内精品久久久久| 久久精品无码午夜福利理论片 | 久久国产热这里只有精品| 精品久久久久久无码中文字幕一区| 久久综合欧美成人| 国产Av激情久久无码天堂| 无码国内精品久久综合88| 久久久久亚洲精品天堂久久久久久| 国产成人久久精品区一区二区| 精品久久久中文字幕人妻| 7777精品伊人久久久大香线蕉| 一本久久免费视频| 日本久久中文字幕| 久久久青草青青国产亚洲免观|