[翻譯] Effective C++, 3rd Edition, Item 31: 最小化檔案之間的 compilation dependencies(編譯依賴)(下)

來源:互聯網
上載者:User

(點擊此處,接上篇)

  • 當 object references(引用)和 pointers(指標)可以做到時就避免使用 objects。僅需一個類型的聲明,你就可以定義到這個類型的 references 和 pointers。而定義一個類型的 objects 必須要存在這個類型的定義。
  • 只要你能做到,就用對 class declarations(類聲明)的依賴替代對 class definitions(類定義)的依賴。注意在你聲明一個使用一個 class 的函數時絕對不需要有這個 class definition,即使這個函數通過傳值方式傳遞或返回這個 class:

class Date;                        // class declaration

Date today();                      // fine — no definition
void clearAppointments(Date d);    // of Date is needed

當然,pass-by-value(傳值)通常不是一個好主意(參見 Item 20),但是如果你發現你自己因為某種原因而使用它,依然不能為引入不必要的 compilation dependencies(編譯依賴)辯解。

不定義 Date 就可以聲明 today 和 clearAppointments 的能力可能會令你感到驚奇,但是它其實並不像看上去那麼不同尋常。只有有人調用了這些函數,Date 的定義才必須在調用之前被看到。為什麼費心去聲明沒有人調用的函數,你覺得奇怪嗎?很簡單。並不是沒有人調用它們,而是並非每個人都要調用它們。如果你有一個包含很多 function declarations(函式宣告)的庫,每一個客戶都要調用每一個函數是不太可能的。通過將提供 class definitions(類定義)的責任從你的 function declarations(函式宣告)的標頭檔轉移到客戶的包含 function calls(函數調用)的檔案,你就消除了客戶對他們並不真正需要的 type definitions(類型定義)的人為依賴。

  • 為 declarations(聲明)和 definitions(定義)分別提供標頭檔。為了便于堅持上面的指導方針,標頭檔需要成對出現:一個用於 declarations(聲明),另一個用於 definitions(定義)。當然,這些檔案必須保持一致。如果一個 declaration(聲明)在一個地方被改變了,它必須在兩處都被改變。得出的結果是:庫的客戶應該總是 #include 一個 declaration(聲明)檔案,而不是自己 forward-declaring(前向聲明)某些東西,而庫的作者應該提供兩個標頭檔。例如,想要聲明 today 和 clearAppointments 的 Date 的客戶不應該像前面展示的那樣手動前向聲明 Date。更合適的是,它應該 #include 適當的用於 declarations(聲明)的標頭檔:

#include "datefwd.h"            // header file declaring (but not
                                // defining) class Date

Date today();                   // as before
void clearAppointments(Date d);

declaration-only(僅有聲明)的標頭檔的名字 "datefwd.h" 基於來自標準 C++ 庫(參見 Item 54)的標頭檔 <iosfwd>。<iosfwd> 包含 iostream 組件的 declarations(聲明),而它們相應的 definitions(定義)在幾個不同的標頭檔中,包括 <sstream>,<streambuf>,<fstream> 和 <iostream>。

<iosfwd> 在其它方面也有啟發意義,而且它表明本 Item 的建議對於 templates(模板)和 non-templates(非模板)一樣有效。儘管 Item 30 解釋了在很多構建環境中,template definitions(模板定義)的典型特徵是位於標頭檔中,但有些環境允許 template definitions(模板定義)位於非標頭檔中,所以為模板提供一個 declaration-only(僅有聲明)的標頭檔依然是有意義的。<iosfwd> 就是一個這樣的標頭檔。

C++ 還提供了 export 關鍵字允許將 template declarations(模板聲明)從 template definitions(模板定義)中分離出來。不幸的是,支援 export 的編譯器非常少見,而與 export 打交道的實際經驗就更少了。結果是,現在就說 export 在高效 C++ 編程中扮演什麼角色還為時尚早。

像 Person 這樣的使用 pimpl idiom(慣用法)的 classes 經常被稱為 Handle classes。為了避免你對這樣的 classes 實際上做什麼事的好奇心,一種方法是將所有對它們的函數調用都轉送給相應的 implementation classes(實作類別),而讓那些 classes 來做真正的工作。例如,這是兩個 Person 的 member functions(成員函數)被實現的例子:

#include "Person.h"          // we're implementing the Person class,
                             // so we must #include its class definition

#include "PersonImpl.h"      // we must also #include PersonImpl's class
                             // definition, otherwise we couldn't call
                             // its member functions; note that
                             // PersonImpl has exactly the same
                             // member functions as Person — their
                             // interfaces are identical

Person::Person(const std::string& name, const Date& birthday,
               const Address& addr)
: pImpl(new PersonImpl(name, birthday, addr))
{}

std::string Person::name() const
{
  return pImpl->name();
}

注意 Person 的 constructor(建構函式)是如何調用 PersonImpl 的 constructor(建構函式)的(通過使用 new ——參見 Item 16),以及 Person::name 是如何調用 PersonImpl::name 的。這很重要。使 Person 成為一個 Handle class 不需要改變 Person 要做的事情,僅僅是改變了它做事的方法。

另一個不同於 Handle class 的候選方法是使 Person 成為一個被叫做 Interface class 的特殊種類的 abstract base class(抽象基類)。這樣一個 class 的作用是為 derived classes(衍生類別)指定一個 interface(介面)(參見 Item 34)。結果,它的典型特徵是沒有 data members(資料成員),沒有 constructors(建構函式),有一個 virtual destructor(虛擬解構函式)(參見 Item 7)和一組指定 interface(介面)的 pure virtual functions(純虛擬函數)。

Interface classes 類似 Java 和 .NET 中的 Interfaces,但是 C++ 並不會為 Interface classes 強加那些 Java 和 .NET 為 Interfaces 強加的種種限制。例如,Java 和 .NET 都不允許 Interfaces 中有 data members(資料成員)和 function implementations(函數實現),但是 C++ 不禁止這些事情。C++ 的較大彈性是有用處的。就像 Item 36 解釋的,在一個 hierarchy(繼承體系)的所有 classes 中 non-virtual functions(非虛擬函數)的實現應該相同,因此將這樣的函數實現為聲明它們的 Interface class 的構件就是有意義的。

一個 Person 的 Interface class 可能就像這樣:

class Person {
public:
  virtual ~Person();

  virtual std::string name() const = 0;
  virtual std::string birthDate() const = 0;
  virtual std::string address() const = 0;
  ...
};

這個 class 的客戶必須針對 Person 的指標和引用進行編程,因為執行個體化包含 pure virtual functions(純虛擬函數)的 classes 是不可能的。(然而,執行個體化從 Person 派生的 classes 是可能的——參見後面。)和 Handle classes 的客戶一樣,除非 Interface class 的 interface(介面)發生變化,否則 Interface classes 的客戶不需要重新編譯。

一個 Interface class 的客戶必須有辦法建立新的 objects。他們一般通過調用一個為“可以真正執行個體化的 derived classes(衍生類別)”扮演 constructor(建構函式)角色的函數做到這一點的。這樣的函數一般稱為 factory functions(參見 Item 13)或 virtual constructors(虛擬建構函式)。他們返回指向動態分配的支援 Interface class 的 interface 的 objects 的指標(smart pointers(智能指標)更合適——參見 Item 18)。這樣的函數在 Interface class 內部一般聲明為 static:

class Person {
public:
 ...

 static std::tr1::shared_ptr<Person>    // return a tr1::shared_ptr to a new
   create(const std::string& name,      // Person initialized with the
          const Date& birthday,         // given params; see Item 18 for
          const Address& addr);         // why a tr1::shared_ptr is returned
 ...
};

客戶就像這樣使用它們:

std::string name;
Date dateOfBirth;
Address address;
...

// create an object supporting the Person interface
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));

...

std::cout << pp->name()                 // use the object via the
          << " was born on "            // Person interface
          << pp->birthDate()
          << " and now lives at "
          << pp->address();
...                                     // the object is automatically
                                        // deleted when pp goes out of
                                        // scope — see Item 13

當然,在某些場合,必須定義支援 Interface class 的 interface(介面)的 concrete classes (具體類)並調用真正的 constructors(建構函式)。這所有的一切都發生在幕後,隱藏在那個包含了 virtual constructors(虛擬建構函式)的實現的檔案之內。例如,Interface class Person 可以有一個 concrete derived class(具體衍生類別)RealPerson,它為繼承到的 virtual functions(虛擬函數)提供了實現:

class RealPerson: public Person {
public:
  RealPerson(const std::string& name, const Date& birthday,
             const Address& addr)
  : theName(name), theBirthDate(birthday), theAddress(addr)
  {}

  virtual ~RealPerson() {}

  std::string name() const;        // implementations of these
  std::string birthDate() const;   // functions are not shown, but
  std::string address() const;     // they are easy to imagine

private:
  std::string theName;
  Date theBirthDate;
  Address theAddress;
};

給出了 RealPerson,寫 Person::create 就微不足道了:

std::tr1::shared_ptr<Person> Person::create(const std::string& name,
                                            const Date& birthday,
                                            const Address& addr)
{
  return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday,addr));
}

Person::create 的一個更現實的實現會依賴於諸如,另外的函數參數的值,從檔案或資料庫讀出的資料,環境變數等等,建立不同 derived class(衍生類別)類型的 objects。

RealPerson 示範了實現一個 Interface class 的兩種最通用的機制中的一種:從 Interface class(Person)繼承它的 interface specification(介面規格),然後實現 interface(介面)中的函數。實現一個 Interface class 的第二種方法包含 multiple inheritance(多繼承),在 Item 40 中探討這個話題。

Handle classes 和 Interface classes 從 implementations(實現)中分離出 interfaces(介面),因此減少了檔案之間的編譯依賴。如果你是一個喜好挖苦的人,我知道你正在找小號字型寫成的限制。“所有這些把戲會騙走我什麼呢?”你小聲嘀咕著。答案在電腦科學中非常平常:它會消耗一些運行時的速度,另外每個 object 會佔用一些額外的記憶體。

在 Handle classes 的情況下,member functions 必須通過 implementation pointer(實現的指標)得到 object 的資料。這就在每次訪問中增加了一個間接層。而且你必須在儲存每一個 object 所需的記憶體量中增加這一 implementation pointer(實現的指標)的大小。最後,這一 implementation pointer(實現的指標)必須被初始化(在 Handle class 的 constructors(建構函式)中)為指向一個動態分配的 implementation object(實現的對象),所以你要承受動態記憶體分配(以及隨後的釋放)所固有的成本和遭遇 bad_alloc (out-of-memory) exceptions(異常)的可能性。

對於 Interface classes,每一個函數調用都是虛擬,所以你每調用一次函數就要支付一個間接跳轉的成本(參見 Item 7)。還有,從 Interface class 派生的 objects 必須包含一個 virtual table pointer(還是參見 Item 7)。這個 pointer 可能增加儲存一個 object 所需的記憶體量,這依賴於這個 Interface class 是否是這個 object 的 virtual functions(虛擬函數)的唯一來源。

最後,無論 Handle classes 還是 Interface classes 都不能大量使用 inline functions(內嵌函式)。Item 30 解釋了為什麼一般情況下函數本體必須在標頭檔中才能做到 inline,但是 Handle classes 和 Interface classes 一般都被設計用於隱藏類似函數本體這樣的實現細節。

然而,僅僅因為它們所涉及到的成本而放棄 Handle classes 和 Interface classes 會成為一個嚴重的錯誤。virtual functions(虛擬函數)也是一樣,但你還是不能放棄它們,對嗎?(如果能,你看錯書了。)替代做法是,考慮以一種改進的方式使用這些技術。在開發過程中,使用 Handle classes 和 Interface classes 來最小化當實現發生變化時對客戶的影響。當能看出在速度和/或大小上的不同足以證明增加 classes 之間的耦合是值得的時候,可以用 concrete classes(具體類)取代 Handle classes 和 Interface classes 供產品使用。

Things to Remember

  • 最小化編譯依賴後面的一般想法是用對 declarations(聲明)的依賴取代對 definitions(定義)的依賴。基於此想法的兩個方法是 Handle classes 和 Interface classes。
  • 庫標頭檔應該以完整並且 declaration-only(只有聲明)的形式存在。無論是否包含 templates(模板)都適用於這一點。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.