青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

陳碩的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 陳碩 閱讀(12318) 評論(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); 任何庫都要且緊要這一個函數作為接口。是這意思么?
  回復  更多評論   

<2025年9月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

導航

統計

常用鏈接

隨筆分類

隨筆檔案

相冊

搜索

最新評論

閱讀排行榜

評論排行榜

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <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>
            蜜桃av一区二区三区| 亚洲与欧洲av电影| 欧美成人免费观看| 久久久青草婷婷精品综合日韩| 亚洲免费在线精品一区| 亚洲国产成人av好男人在线观看| 欧美一区二区三区四区夜夜大片| 在线视频亚洲欧美| 亚洲影院色无极综合| 中文成人激情娱乐网| 99精品视频一区| 欧美亚洲免费在线| 久久精品99久久香蕉国产色戒| 欧美在线一二三四区| 美女国产精品| 亚洲激情网站| 一本色道久久综合亚洲精品不卡 | 老司机成人网| 欧美二区在线观看| 国产精品一区二区三区四区| 国产精品免费一区二区三区在线观看| 欧美视频在线一区| 在线观看久久av| 亚洲免费观看在线观看| 在线视频亚洲一区| 欧美成年人网| 亚洲精品影视| 午夜精品久久久久久久99水蜜桃| 亚洲视频综合在线| 午夜在线观看免费一区| 久久gogo国模裸体人体| 91久久精品一区二区别| 一区二区三区日韩欧美精品| 亚洲欧美三级伦理| 欧美激情一区二区三区在线| 国产精品第三页| 狠狠v欧美v日韩v亚洲ⅴ| 亚洲午夜精品久久久久久app| 久久国产精品99精品国产| 欧美成人激情视频| 亚洲美女91| 久久婷婷成人综合色| 亚洲国产一区二区三区高清| 亚洲国产精品小视频| 香蕉av777xxx色综合一区| 久久在线免费视频| 国产精品亚洲人在线观看| 夜夜嗨av一区二区三区网页 | av成人天堂| 久久这里只精品最新地址| 亚洲在线网站| 欧美激情精品久久久久久变态| 国产精品一级| 性欧美xxxx视频在线观看| 亚洲福利在线看| 香蕉乱码成人久久天堂爱免费 | 午夜免费电影一区在线观看| 欧美韩日视频| 影音先锋日韩有码| 久久午夜羞羞影院免费观看| 夜色激情一区二区| 欧美国产在线电影| 日韩亚洲不卡在线| 欧美激情久久久| 久久精品国产亚洲5555| 黄色在线成人| 久久久久国产精品午夜一区| 中文精品一区二区三区| 国产精品综合视频| 午夜精品福利在线| 亚洲午夜久久久久久尤物 | av成人免费在线| 欧美特黄a级高清免费大片a级| 亚洲三级性片| 欧美激情中文字幕乱码免费| 欧美xart系列在线观看| 亚洲国产精品嫩草影院| 免费视频一区二区三区在线观看| 91久久精品国产| 亚洲黄一区二区三区| 免费不卡在线观看| 最新中文字幕一区二区三区| 亚洲理伦在线| 欧美激情影院| 美国十次成人| 亚洲天堂男人| 亚洲一区二区三区午夜| 激情自拍一区| 欧美插天视频在线播放| 欧美大尺度在线观看| 亚洲伊人一本大道中文字幕| 一区二区三区久久网| 国产精品少妇自拍| 美乳少妇欧美精品| 欧美aⅴ一区二区三区视频| 亚洲精品专区| 亚洲欧美综合网| 黑人操亚洲美女惩罚| 欧美va亚洲va日韩∨a综合色| 欧美日韩国产一区| 亚洲欧美电影在线观看| 亚洲欧美日韩人成在线播放| 久久伊人一区二区| 99精品视频一区| 亚洲一区三区在线观看| 欧美性猛交视频| 免费在线日韩av| 欧美极品一区二区三区| 最新日韩精品| 先锋影音久久| 91久久中文字幕| 中文网丁香综合网| 亚洲精品亚洲人成人网| 亚洲欧美韩国| 亚洲九九九在线观看| 久久久99久久精品女同性| 亚洲精品影院在线观看| 亚洲免费视频在线观看| 亚洲图片在区色| 久久狠狠一本精品综合网| 日韩视频永久免费观看| 久久综合久久综合久久| 午夜视频在线观看一区| 裸体女人亚洲精品一区| 久久深夜福利| 国产精品国产a级| 欧美高清在线观看| 亚洲电影在线| 午夜精品久久久久久久久久久久| 亚洲精品乱码久久久久久| 久久免费99精品久久久久久| 亚洲中午字幕| 欧美国产1区2区| 亚洲黄色三级| 极品尤物av久久免费看 | 亚洲国产精品成人| 在线欧美福利| 久久成人免费日本黄色| 亚洲制服欧美中文字幕中文字幕| 欧美日韩天天操| 欧美大尺度在线| 国内精品视频在线播放| 久久精品国内一区二区三区| 欧美在线一二三四区| 国产精品看片资源| 亚洲一区二区三区在线| 亚洲午夜在线视频| 一区二区三区高清在线观看| 亚洲综合日韩| 国产精品啊啊啊| 一本大道久久精品懂色aⅴ| 国产精品99久久久久久人| 欧美国内亚洲| 亚洲欧洲日夜超级视频| 亚洲精选久久| 欧美日本在线观看| 亚洲精品欧美日韩专区| 国产精品一区免费在线观看| 一个色综合导航| 亚洲欧美日韩在线| 国模精品一区二区三区色天香| 久久国产精品黑丝| 欧美日韩国产黄| 亚洲日本va午夜在线电影| 亚洲一区二区三区激情| 国内外成人免费视频| 欧美成人精品在线观看| 亚洲一区二区三区乱码aⅴ蜜桃女 亚洲一区二区三区乱码aⅴ | 久久精品国产v日韩v亚洲| 欧美激情欧美激情在线五月| 亚洲深夜激情| 一区二区三区中文在线观看| 欧美顶级少妇做爰| 亚洲影院色在线观看免费| 欧美国产日本在线| 欧美伊人久久| 一二三区精品| 亚洲成人在线视频网站| 国产精品久久久久久久午夜片| 久久久五月天| 亚洲欧美自拍偷拍| 日韩一级免费| 欧美成人亚洲| 久久精品亚洲精品国产欧美kt∨| 一区二区三区日韩欧美| 在线视频国产日韩| 国产日韩久久| 国产精品日日摸夜夜摸av| 欧美大片在线看免费观看| 久久aⅴ国产欧美74aaa| 亚洲视频每日更新| 亚洲日本理论电影| 欧美成人免费小视频| 久久精品国产综合精品| 亚洲午夜性刺激影院| 亚洲乱码视频| 91久久嫩草影院一区二区| 影音先锋亚洲电影| 国产有码在线一区二区视频| 国产精品美女一区二区在线观看|