• <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++ 工程實踐(5):避免使用虛函數作為庫的接口

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice

             

            摘要:作為 C++ 動態庫的作者,應當避免使用虛函數作為庫的接口。這么做會給保持二進制兼容性帶來很大麻煩,不得不增加很多不必要的 interfaces,最終重蹈 COM 的覆轍。

            本文主要討論 Linux x86 平臺,會繼續舉 Windows/COM 作為反面教材。

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

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

            C++ 程序庫的作者的生存環境

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

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

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

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

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

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

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

            也就是說,在設計庫的時候必須要考慮將來如何升級

            基于以上兩個基本假設來做決定。第一個決定很好做,如果需要 hot fix,那么只能用動態庫;否則,在分布式系統中使用靜態庫更容易部署,這在前文中已經談過。(“動態庫比靜態庫節約內存”這種優勢在今天看來已不太重要。)

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

            第二個決定不是那么容易做,關鍵問題是,要選擇一種可擴展的 (extensible) 接口風格,讓庫的升級變得更輕松。“升級”有兩層意思:

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

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

            虛函數作為庫的接口的兩大用途

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

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

            對于“回調”方式,現代 C++ 有更好的做法,即 boost::function + boost::bind,見參考文獻[4],muduo 的回調全部采用這種新方法,見《Muduo 網絡編程示例之零:前言》。本文以下不考慮以虛函數為回調的過時做法。

            對于“調用”方式,這里舉一個虛構的圖形庫,這個庫的功能是畫線、畫矩形、畫圓弧:

               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: };

            這里略去了很多與本文主題無關細節,比如 Graphics 的構造與析構、draw*() 函數應該是 public、Graphics 應該不允許復制,還比如 Graphics 可能會用 pure virtual functions 等等,這些都不影響本文的討論。

            這個 Graphics 庫的使用很簡單,客戶端看起來是這個樣子。

            Graphics* g = getGraphics();

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

            releaseGraphics(g); g = NULL;

            似乎一切都很好,陽光明媚,符合“面向對象的原則”,但是一旦考慮升級,前景立刻變得昏暗。

            虛函數作為接口的弊端

            以虛函數作為接口在二進制兼容性方面有本質困難:“一旦發布,不能修改”。

            假如我需要給 Graphics 增加幾個繪圖函數,同時保持二進制兼容性。這幾個新函數的坐標以浮點數表示,我理想中的新接口是:

            --- 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++ 二進制兼容性方面的限制,我們不能這么做。其本質問題在于 C++ 以 vtable[offset] 方式實現虛函數調用,而 offset 又是根據虛函數聲明的位置隱式確定的,這造成了脆弱性。我增加了 drawLine(double x0, double y0, double x1, double y1),造成 vtable 的排列發生了變化,現有的二進制可執行文件無法再用舊的 offset 調用到正確的函數。

            怎么辦呢?有一種危險且丑陋的做法:把新的虛函數放到 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);
             };

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

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

            1. 通過鏈式繼承來擴展現有 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);
            +};

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

            2. 通過多重繼承來擴展現有 interface,例如定義一個與 Graphics class 有同樣成員的 Graphics2,再讓實現同時繼承這兩個 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);
            +};
            +
            +// 在實現中采用多重接口繼承
            +class GraphicsImpl : public Graphics,  // version 1
            +                     public Graphics2, // version 2
            +{
            +  // ...
            +};

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

            在我看來帶版本的 interface 實在是很丑陋,因為每次改動都引入了新的 interface class,會造成日后客戶端代碼難以管理。比如,如果代碼使用了 Graphics3 的功能,要不要把現有的 Graphics2 都替換掉?

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

            這種二難境地純粹是“以虛函數為庫的接口”造成的。如果我們能直接原地擴充 class Graphics,就不會有這些屁事,見本文“推薦做法”一節。

            假如 Linux 系統調用以 COM 接口方式實現

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

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

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

             

            不要誤認為“接口一旦發布就不能更改”是天經地義的,那不過是“以 C++ 虛函數為接口”的固有弊端,如果跳出這個框框去思考,其實 C++ 庫的接口很容易做得更好。

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

            相反,Linux 系統調用以編譯期常數方式固定下來,萬年不變,輕而易舉地解決了這個問題。在其他面向對象語言(Java/C#)中,我也沒有見過每改動一次就給 interface 遞增版本號的詭異做法。

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

             

            動態庫的接口的推薦做法

            取決于動態庫的使用范圍,有兩類做法。

            如果,動態庫的使用范圍比較窄,比如本團隊內部的兩三個程序在用,用戶都是受控的,要發布新版本也比較容易協調,那么不用太費事,只要做好發布的版本管理就行了。再在可執行文件中使用 rpath 把庫的完整路徑確定下來。

            比如現在 Graphics 庫發布了 1.1.0 和 1.2.0 兩個版本,這兩個版本可以不必是二進制兼容。用戶的代碼從 1.1.0 升級到 1.2.0 的時候要重新編譯一下,反正他們要用新功能都是要重新編譯代碼的。如果要原地打補丁,那么 1.1.1 應該和 1.1.0 二進制兼容,而 1.2.1 應該和 1.2.0 兼容。如果要加入新的功能,而新的功能與 1.2.0 不兼容,那么應該發布到 1.3.0 版本。

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

             

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

            1. 暴露的接口里邊不要有虛函數,而且 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. 在庫的實現中把調用轉發 (forward) 給實現 Graphics::Impl ,這部分代碼位于 .so/.dll 中,隨庫的升級一起變化。

            #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. 如果要加入新的功能,不必通過繼承來擴展,可以原地修改,且很容易保持二進制兼容性。先動頭文件:

            --- 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;
             };

            然后在實現文件里增加 forward,這么做不會破壞二進制兼容性,因為增加 non-virtual 函數不影響現有的可執行文件。

            --- 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 的手續,帶來的好處是可擴展性與二進制兼容性,通常是劃算的。pimpl 扮演了編譯器防火墻的作用。

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

             

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

             

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

            本文只談了使用 class 為接口,其實用 free function 有時候更好(比如 muduo/base/Timestamp.h 除了定義 class Timestamp,還定義了 muduo::timeDifference() 等 free function),這也是 C++ 比 Java 等純面向對象語言優越的地方。留給將來再細談吧。

            參考文獻

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

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

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

            [4] 陳碩,《以 boost::function 和 boost:bind 取代虛函數》,《樸實的 C++ 設計》。

            知識共享許可協議
            作品采用知識共享署名-非商業性使用-相同方式共享 3.0 Unported許可協議進行許可。

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

            評論

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

            vtable[offset] 和系統調用有本質的不同嗎?  回復  更多評論   

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

            其實大多數問題都歸結于:人們不肯使用單一版本的編譯器編譯軟件所依賴的所有庫

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

            何苦呢,調試就無所謂了,反正release嘛就都用相同的東西編譯了好了,現在網絡帶寬那么大,就算你要升級,幾個dll的尺寸算毛。

            除非,你用的庫沒源代碼,那就算了,庫的作者們都會extern "C"的。  回復  更多評論   

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

            @yrj
            請先定義“本質”,explicit 與 implicit 算不算本質?  回復  更多評論   

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

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

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

            @陳碩
            我覺得 vtable[offset] 的 offset 和系統調用編號是一樣的,當然它們的實現是不同的。增加新功能要保持二進制兼容,offset 或系統調用編號都不能變。  回復  更多評論   

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

            非常認同博主的觀點,尤其是庫的作者和使用者是不同的人群的時候,非常好。  回復  更多評論   

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

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

            mark  回復  更多評論   

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

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

            <2011年2月>
            303112345
            6789101112
            13141516171819
            20212223242526
            272812345
            6789101112

            導航

            統計

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            国产精品久久久久免费a∨| 粉嫩小泬无遮挡久久久久久| 久久99精品久久久久久齐齐| 国产精品xxxx国产喷水亚洲国产精品无码久久一区 | 亚洲日本va中文字幕久久| 欧美无乱码久久久免费午夜一区二区三区中文字幕 | 国产精品成人99久久久久| 精品免费久久久久国产一区| 日韩一区二区三区视频久久| 国产精品禁18久久久夂久| 韩国三级中文字幕hd久久精品| 色天使久久综合网天天| 奇米综合四色77777久久| 四虎国产永久免费久久| 性做久久久久久久久老女人| 69久久夜色精品国产69| 久久久久亚洲爆乳少妇无 | 久久久久久亚洲AV无码专区| 国产免费久久精品丫丫| 国产婷婷成人久久Av免费高清| 国产精品伊人久久伊人电影 | 狠狠色丁香久久婷婷综合五月 | 久久天天躁狠狠躁夜夜躁2014| 久久久久久亚洲AV无码专区| 国产精品VIDEOSSEX久久发布| 久久久久亚洲精品天堂| 超级97碰碰碰碰久久久久最新| 国产91久久综合| 久久婷婷国产综合精品| 亚洲精品美女久久777777| 7777精品伊人久久久大香线蕉 | 国产精品99久久久久久猫咪| 国产V亚洲V天堂无码久久久| 久久亚洲欧美国产精品| 色欲综合久久躁天天躁蜜桃| 伊人色综合久久天天网| 色偷偷91久久综合噜噜噜噜| 中文国产成人精品久久亚洲精品AⅤ无码精品 | 99国产欧美久久久精品蜜芽| 久久精品人人做人人爽97 | 精品久久综合1区2区3区激情|