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

陳碩的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不卡在线观看| 日韩一级大片| 一区二区三区四区五区视频| 99riav1国产精品视频| 亚洲一区久久久| 久久久久久夜| 欧美日韩专区在线| 国产综合久久久久久| 91久久精品国产| 亚洲欧美日韩电影| 欧美成人dvd在线视频| 亚洲精品国产精品乱码不99 | 欧美日韩一区三区| 欧美成人精品一区二区| 日韩一级免费观看| 亚洲伊人网站| 狼人天天伊人久久| 亚洲精品久久嫩草网站秘色| 一区二区久久久久久| 久久精品2019中文字幕| 欧美成人资源网| 国产精品综合久久久| 亚洲精品一区二区三| 久久精品五月| 一区二区三区四区国产| 你懂的一区二区| 国产综合激情| 香蕉成人伊视频在线观看| 欧美激情网站在线观看| 欧美一区二区女人| 国产精品欧美风情| 亚洲视频免费在线| 亚洲黄色性网站| 久久久成人网| 国产亚洲成av人在线观看导航 | 欧美夫妇交换俱乐部在线观看| 国产精品视频一区二区高潮| 亚洲欧美另类在线| 国产精品视频午夜| 久久久国产精品一区二区三区| 嫩草影视亚洲| 黄色小说综合网站| 欧美一区二区三区在线观看视频 | 国产精品午夜在线| 亚洲日韩中文字幕在线播放| 欧美一区二区女人| 亚洲无限av看| 欧美亚州一区二区三区| 亚洲视频一区二区| 亚洲另类自拍| 欧美人与禽猛交乱配| 日韩一区二区精品视频| 亚洲欧洲日产国产网站| 欧美www视频在线观看| 亚洲国内自拍| 亚洲区中文字幕| 欧美日韩一区二区视频在线| 99在线|亚洲一区二区| 亚洲精品视频在线观看免费| 欧美日韩精品在线| 翔田千里一区二区| 久久国产加勒比精品无码| 一区二区在线不卡| 亚洲高清中文字幕| 欧美三级电影一区| 欧美一区二区免费| 久久久久中文| 99精品久久久| 亚洲一区三区电影在线观看| 国产欧美日本一区二区三区| 久久亚洲一区二区三区四区| 免费看成人av| 亚洲女同精品视频| 久久成人一区二区| 99精品国产在热久久下载| 亚洲一区二区三区激情| 激情文学综合丁香| 日韩系列在线| 韩国三级电影久久久久久| 亚洲国产女人aaa毛片在线| 国产精品成人午夜| 免费亚洲一区| 国产精品成av人在线视午夜片| 久久se精品一区精品二区| 蘑菇福利视频一区播放| 亚洲影视在线| 久热精品视频在线观看一区| 亚洲午夜久久久久久久久电影网| 欧美一区二区视频在线| 99精品欧美| 久久亚洲综合网| 亚洲砖区区免费| 免费在线成人av| 久久久99国产精品免费| 99re6热在线精品视频播放速度| 午夜老司机精品| 影音欧美亚洲| 亚洲视频1区2区| 91久久精品国产91久久性色tv| 中日韩高清电影网| 亚洲美女诱惑| 久久人人97超碰精品888| 亚洲欧美另类中文字幕| 欧美成人中文字幕在线| 久久影院午夜论| 国产伦精品一区二区三区视频黑人| 男女激情视频一区| 国产一区二区三区高清| 亚洲少妇在线| 国产精品99久久久久久宅男| 免费成人在线观看视频| 久久视频一区二区| 国产偷久久久精品专区| 一区二区三区精品视频| 一区二区三区日韩| 欧美—级高清免费播放| 欧美黄色网络| 亚洲国产精品成人一区二区| 午夜在线一区二区| 欧美一区二区三区视频| 国产精品免费电影| 中国日韩欧美久久久久久久久| 亚洲视频在线观看| 欧美日韩免费观看一区二区三区| 亚洲福利专区| 亚洲欧洲一区二区在线播放| 久久夜色精品国产欧美乱极品| 久久久久久久综合日本| 国产亚洲永久域名| 欧美在线综合| 久久夜色精品国产欧美乱| 国产午夜一区二区三区| 久久精品国产视频| 久久深夜福利免费观看| 激情亚洲网站| 久久综合九色99| 欧美国产一区二区三区激情无套| 伊人久久婷婷| 欧美国产欧美综合| 亚洲精品午夜| 午夜激情一区| 国内自拍一区| 欧美va亚洲va国产综合| 亚洲精品一区二区三区在线观看 | 一区二区久久久久| 午夜精品短视频| 国产又爽又黄的激情精品视频 | 葵司免费一区二区三区四区五区| 欧美成人第一页| 一本大道久久a久久精品综合| 欧美日韩精品一区二区| 亚洲在线国产日韩欧美| 可以免费看不卡的av网站| 最新高清无码专区| 欧美午夜不卡在线观看免费 | 欧美激情综合色| 91久久黄色| 午夜精品亚洲| 影院欧美亚洲| 欧美另类一区二区三区| 亚洲一区亚洲| 欧美电影打屁股sp| 亚洲图片激情小说| 国内揄拍国内精品久久| 欧美激情91| 欧美亚洲综合网| 亚洲精品午夜| 久久综合伊人77777蜜臀| 中文一区二区| 在线看成人片| 国产免费一区二区三区香蕉精| 老司机亚洲精品| 亚洲欧美www| 亚洲日韩第九十九页| 久久九九精品| 亚洲午夜高清视频| 91久久精品国产91久久| 国产在线精品成人一区二区三区 | 欧美激情第六页| 欧美一级网站| 在线视频精品一| 在线观看视频欧美| 国产伦精品一区二区三区视频孕妇| 免费观看在线综合| 久久久国产精品一区二区三区| 99国产精品国产精品毛片| 欧美激情91| 欧美成人免费网站| 蜜臀91精品一区二区三区|