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

陳碩的Blog

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

陳碩 (giantchen_AT_gmail)

Blog.csdn.net/Solstice

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

   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)細節(jié),比如 Graphics 的構(gòu)造與析構(gòu)、draw*() 函數(shù)應(yīng)該是 public、Graphics 應(yīng)該不允許復(fù)制,還比如 Graphics 可能會用 pure virtual functions 等等,這些都不影響本文的討論。

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

Graphics* g = getGraphics();

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

releaseGraphics(g); g = NULL;

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

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

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

假如我需要給 Graphics 增加幾個繪圖函數(shù),同時保持二進制兼容性。這幾個新函數(shù)的坐標以浮點數(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++ 二進制兼容性方面的限制,我們不能這么做。其本質(zhì)問題在于 C++ 以 vtable[offset] 方式實現(xiàn)虛函數(shù)調(diào)用,而 offset 又是根據(jù)虛函數(shù)聲明的位置隱式確定的,這造成了脆弱性。我增加了 drawLine(double x0, double y0, double x1, double y1),造成 vtable 的排列發(fā)生了變化,現(xiàn)有的二進制可執(zhí)行文件無法再用舊的 offset 調(diào)用到正確的函數(shù)。

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

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

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

1. 通過鏈式繼承來擴展現(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ù)增加功能,那么還會有 class Graphics3 : public Graphics2;以及 class Graphics4 : public Graphics3 等等。這么做和前面的做法一樣丑陋,因為新的 drawLine(double x0, double y0, double x1, double y1) 函數(shù)位于派生 Graphics2 interace 中,沒有和原來的 drawLine() 函數(shù)呆在一起,造成割裂。

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

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

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

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

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

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

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

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

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

 

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

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

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

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

 

動態(tài)庫的接口的推薦做法

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

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

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

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

 

如果庫的使用范圍很廣,用戶很多,各家的 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. 在庫的實現(xiàn)中把調(diào)用轉(zhuǎn)發(fā) (forward) 給實現(xiàn) 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;
 };

然后在實現(xiàn)文件里增加 forward,這么做不會破壞二進制兼容性,因為增加 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ù),帶來的好處是可擴展性與二進制兼容性,通常是劃算的。pimpl 扮演了編譯器防火墻的作用。

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

 

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

 

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

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

參考文獻

[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ù)接口放入同一個 namespace。

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

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

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

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

評論

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

mark  回復(fù)  更多評論   

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

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

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

導(dǎo)航

統(tǒng)計

常用鏈接

隨筆分類

隨筆檔案

相冊

搜索

最新評論

閱讀排行榜

評論排行榜

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <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片| 一本久道久久久| 欧美精品久久99久久在免费线| 精品91免费| 国产一区二区三区在线观看免费 | 欧美激情一二区| 免费亚洲电影在线观看| 欧美激情一级片一区二区| 欧美日韩国产一区二区| 国产精品久久久久aaaa九色| 国产精品区一区| 国产精品一香蕉国产线看观看| 久久精品一级爱片| 免费日韩一区二区| 国产精品伦子伦免费视频| 国产亚洲va综合人人澡精品| 久久男女视频| 亚洲精品一区久久久久久| 亚洲一区视频在线| 麻豆久久婷婷| 国产精品久久久久久久免费软件| 久久一二三四| 欧美少妇一区| 亚洲黄色片网站| 午夜精品久久久久久久男人的天堂 | 国产精品嫩草99av在线| 国产日韩欧美视频在线| 亚洲国产精品123| 男人的天堂成人在线| 亚洲国产成人av| 亚洲精品一区二区三区99| 亚洲欧美日韩在线一区| 欧美精品在线网站| 黄色小说综合网站| 性欧美videos另类喷潮| 亚洲日本欧美在线| 久久人人爽人人爽爽久久| 国产精品国产一区二区| 亚洲精品久久久久中文字幕欢迎你| 国产日韩在线一区二区三区| 亚洲精品在线观看免费| 久久精品一区蜜桃臀影院| 日韩一区二区精品在线观看| 猛男gaygay欧美视频| 黑人中文字幕一区二区三区| 欧美一级理论片| 亚洲美洲欧洲综合国产一区| 欧美xxxx在线观看| 最新国产成人av网站网址麻豆| 在线观看亚洲精品视频| 亚洲女性裸体视频| 99国产精品久久久久久久| 欧美成人精品一区二区三区| 亚洲国产成人久久| 欧美成人亚洲成人日韩成人| 久久久av水蜜桃| 精品1区2区| 欧美视频国产精品| 一区二区三区四区在线| 亚洲国产欧美不卡在线观看| 免费在线欧美黄色| 亚洲精品久久久一区二区三区| 亚洲精品久久久久久久久久久| 狠狠色噜噜狠狠色综合久| 欧美亚洲在线观看| 欧美一区二区三区视频| 国产欧美一区二区精品性| 先锋影音一区二区三区| 亚洲欧美日韩天堂一区二区| 国产在线视频欧美一区二区三区| 亚洲精品国产精品国自产在线| 99热精品在线| 日韩视频免费在线| 国产精品试看| 久久综合导航| 蜜桃久久av一区| 中国成人在线视频| 亚洲免费视频成人| 伊人久久亚洲影院| 91久久嫩草影院一区二区| 欧美日韩亚洲系列| 久久精品中文字幕免费mv| 噜噜噜久久亚洲精品国产品小说| 欧美日韩亚洲系列| 亚洲欧美国产77777| 欧美一区二区黄| 亚洲精品一区二区三区99| 一区二区免费看| 在线亚洲一区二区| 久久人人97超碰人人澡爱香蕉| 欧美极品影院| 一区二区三区色| 欧美伊人久久| 亚洲人成毛片在线播放| 夜久久久久久| 亚洲国产91| 亚洲图片欧洲图片av| 雨宫琴音一区二区在线| 亚洲精品五月天| 国产视频欧美视频| 日韩视频中午一区| 精品1区2区| 亚洲永久字幕| 亚洲日本免费电影| 欧美亚洲在线播放| 亚洲调教视频在线观看| 久久国产精品第一页| 一区二区日韩精品| 母乳一区在线观看| 久久久久久久一区二区三区| 欧美精品久久久久久| 久久久精品久久久久| 亚洲国产另类精品专区| 国产亚洲电影| 亚洲精选视频免费看| 在线不卡免费欧美| 欧美一区综合| 亚洲欧美国产高清| 欧美精品成人| 欧美国产激情| 国产婷婷色综合av蜜臀av| 亚洲免费观看高清在线观看 | 国产欧美日韩一级| 亚洲国产精品视频| 极品少妇一区二区三区| 亚洲欧美国产精品桃花| 一本色道久久综合亚洲精品按摩 | 亚洲午夜av电影| 亚洲日本免费电影| 久久一二三四| 老牛嫩草一区二区三区日本| 看片网站欧美日韩| 蜜臀av性久久久久蜜臀aⅴ| 国产精品视频精品| 亚洲调教视频在线观看| 亚洲午夜一区二区三区| 欧美激情亚洲视频| 亚洲高清自拍| 亚洲欧洲视频| 欧美精品成人91久久久久久久| av不卡在线看| 模特精品裸拍一区| 亚洲激情不卡| 一本色道久久综合狠狠躁篇怎么玩| 亚洲国产美女久久久久| 亚洲国产人成综合网站| 久久久国产精品一区| 久久综合中文| 在线播放豆国产99亚洲| 欧美va天堂| 女女同性女同一区二区三区91| 亚洲国产精品女人久久久| 日韩一区二区精品| 亚洲欧美色一区| 国产女同一区二区| 欧美伊人久久久久久午夜久久久久| 伊大人香蕉综合8在线视| 午夜精品一区二区三区四区 | 亚洲三级视频| 一区二区三区黄色| 欧美四级伦理在线| 亚洲欧美日韩一区二区三区在线观看| 一区二区三区在线视频观看| 欧美一级理论性理论a| 国产欧美日韩综合一区在线播放 | 一本色道久久88亚洲综合88| 亚洲视频中文| 国产亚洲综合精品| 久久久久久久一区二区| 欧美激情日韩| 午夜久久久久| 亚洲三级电影在线观看| 国产精品久久久久久妇女6080| 欧美激情视频在线播放| 亚洲精品男同| 国产欧美在线观看| 免费精品视频| 亚洲欧美日韩在线综合| 亚洲精品乱码久久久久久黑人| 黄色一区二区在线观看| 欧美精品三级日韩久久| 亚洲网站在线看| 欧美高清在线播放| 午夜精品电影| 99国产精品久久久久久久成人热 | 日韩视频在线观看一区二区| 欧美性视频网站| 理论片一区二区在线| 亚洲最新视频在线| 老司机精品视频网站| 亚洲伊人色欲综合网| 亚洲激情成人网| 在线日韩一区二区| 国产欧美在线视频| 欧美午夜精品伦理| 久久综合五月| 久久激情综合| 亚洲天堂久久| 9l国产精品久久久久麻豆| 在线欧美电影|