Traits: 類型的else-if-then機製

Andrei Alexandrescu

孟岩 譯

侯捷注﹕本文承譯者孟岩先生應允﹐
轉載於此以饗台灣讀者﹐非常感謝。

未得孟岩先生之同意﹐任何人請勿將此文再做轉載。

以下
紅色為譯注﹐紫色為侯捷個人認為宜再斟酌之處。
淺藍色是侯捷個人閱讀時的神秘標記。

本文系由GB簡碼直接轉為BIG5繁碼﹐並未將大陸慣用術語轉換為台灣慣用術語。


什麼是traits﹐為什麼人們把它認為是 C++ Generic Programming 的重要技術﹖
簡短截說﹐traits如此重要﹐是因為此項技術允許系統在編譯時根據類型作一些決斷﹐
就好像在運行時根據值來作出決斷一樣。更進一步﹐此技術遵循“另增一個間接層”
的諺語﹐解決了不少軟件工程問題﹐traits使您能根據其產生的背景(context)
來作出抉擇。這樣最終的代碼就變得清晰易讀﹐容易維護。如果你正確運用了traits
技術﹐你就能在不付出任何性能和安全代價的同時得到這些好處﹐或者能夠契合其他
解決方案上的需求。
例子﹕Traits不僅是泛型程序設計的核心工具﹐而且我希望以下的例子能夠使你相信﹐
在非常特定的問題中﹐它也是很有用的。
假設你現在正在編寫一個關係數據庫應用程序。可能您一開始用數據庫供應商提供的
API庫來進行反問數據庫的操作。但是理所當然的﹐不久之後你會感到不得不寫一些
包裝函數來組織那些原始的API﹐一方面是為了簡潔﹐另一方面也可以更好地適應
你手上的任務。這就是生活的樂趣所在﹐不是嗎﹖
一個典型的API是這樣的﹕提供一個基本的方法用來把游標(cursor, 一個行集和或
者查詢結果)處的原始數據傳送到內存中。現在我們來寫一個高級的函數﹐用來把某
一列的值取出來﹐同時避免暴露底層的細節。這個函數可能會是這個樣子﹕
(假想的DB API用db或DB開頭)
// Example 1: Wrapping a raw cursor int fetch
// operation.
// Fetch an integer from the
//     cursor "cr"
//     at column "col"
//     in the value "val"
void FetchIntField(db_cursor& cr, 
    unsigned int col, int& val)
{
    // Verify type match
    if (cr.column_type[col] != DB_INTEGER)
        throw std::runtime_error(
        "Column type mismatch");
    // Do the fetch
    db_integer temp;
    if (!db_access_column(&cr, col))
        throw std::runtime_error(
        "Cannot transfer data");
    memcpy(&temp, cr.column_data[col],
        sizeof(temp));
    // Required by the DB API for cleanup
    db_release_column(&cr, col);
    // Convert from the database native type to int
    val = static_cast<int>(temp);
}
這種接口函數我們所有人都可能不得不在某個時候寫上一遍﹐它不好對付但又非常重
要﹐處理了大量細節﹐而且這還只是一個簡單的例子。FetchIntField抽象﹐提供了
高一層次的功能﹐它能夠從游標處取得一個整數﹐不必再擔心那些紛繁的細節。
既然這個函數如此有用﹐我們當然希望儘可能重用它。但是怎麼做﹖一個很重要的泛化
步驟就是讓這個函數能夠處理int之外的類型。為了做到這一點﹐我們得仔細考慮代碼中
跟int類型相關的部份。但首先﹐DB_INTEGER和db_integer是什麼意思﹐它們是打哪兒
來的﹖是這樣﹐關係數據庫供應商通常隨API提供一些type-mapping helpers﹐為其所
支持的每種類型和簡單的結構定義一個符號常量或者typedef﹐把數據庫類型對應到
C/C++類型上。
下面是一段假想的數據庫API頭文件﹕
#define DB_INTEGER 1
#define DB_STRING 2
#define DB_CURRENCY 3
...
typedef long int db_integer;
typedef char     db_string[255];
typedef struct {
    int integral_part;
    unsigned char fractionary_part;
} db_currency;
...
我們試  來寫一個FetchDoubleField函數﹐作為走向泛型化的第一步。此函數從游標處得到
一個double值。數據庫本身提供的類型映像(type mapping)是db_currency﹐但是我們希望
能用double的形式來操作。FetchDoubleField看上去跟FetchIntField很相似﹐簡直就是孿
生兄弟。例2﹕
// Example 2: Wrapping a raw cursor double fetch operation.
//
void FetchDoubleField(db_cursor& cr, unsigned int col, double& val)
{
    if (cr.column_type[col] != DB_CURRENCY)
        throw std::runtime_error("Column type mismatch");
    if (!db_access_column(&cr, col))
        throw std::runtime_error("Cannot transfer data");
    db_currency temp;
    memcpy(&temp, cr.column_data[col], sizeof(temp));
    db_release_column(&cr, col);
    val = temp.integral_part + temp.fractionary_part / 100.;
}
看上去很像FetchIntField吧  我們可不想對每一個類型都寫一個單獨的函數﹐所以
如果能夠在一個地方把FetchIntField, FetchDoubleField以及其他的Fetch函數合
為一體就好了。
我們把這兩片代碼的不同之處列舉如下﹕
  •輸入類型﹕double/int
  •內部類型﹕db_currency/db_integer
  •常數值類型﹕DB_CURRENCY/DB_INTEGER
  •算法﹕一個表達式/static_cast
輸入類型(int/double)與其他幾點之間的對應關係看上去沒什麼規律可循﹐而是很隨意﹐
跟數據庫供應商(恰好)提供的類型關係密切。Template機製本身無能為力﹐它沒有提供
如此先進的類型推理機製。也沒法把不同的類型用繼承關係組織起來﹐因為我們處理的是
原始類型。受到API的限制以及問題本身的底層特性﹐乍看上去我們好像沒轍了。不過我們
還有一條活路。
進入TRAITS大門﹕Traits技術就是用來解決上述問題的﹕把與各種類型相關的代碼片斷合體﹐
並且具有類似and/or結構的能力﹐到時可以根據不同的類型產生不同的變體
Traits依賴顯式模版特殊化(explicit template specialization)機製來獲得這種結果。
這一特性使你可以為每一個特定的類型提供模板類的一個單獨實現﹐見例3﹕
// Example 3: A traits example
//
template <class T>
class SomeTemplate
{
    // generic implementation (1)
    ...
};
// 注意下面特異的語法
template <>
class SomeTemplate<char>
{
    // implementation tuned for char (2)
    ...
};
...
SomeTemplate<int> a;      // will use (1)
SomeTemplate<char*> b;    // will use (1)
SomeTemplate<char> c;     // will use (2)
如果你用char類型來實例化SomeTemplate類模板﹐編譯器會用那個顯式的模板聲明來特殊化。
至於其他的類型﹐當然就用那個通用模板來實例化。這就像一個由類型驅動if-statement。
通常最通用的模板(相當于else部份)最先定義﹐if-statement靠後一點。你甚至可以決定
完全不提供通用的模板﹐這樣只有特定的實例化是允許的﹐其他的都會導致編譯錯誤。
現在我們把這個語言特性跟手上的問題聯繫起來。我們要實現一個模板函數FetchField﹐
用需要讀取的類型作為三數來實例化。在該函數內部﹐我會用一個叫做TypeId的東西代表
那個符號常量﹐當要獲取int型值時它的值就是DB_INTEGER﹐當要獲取double型值時它的
值就是DB_CURRENCY。否則﹐就必須在編譯時報錯。類似的﹐根據要獲取的類型的不同﹐
我們還需要操作不同的API類型(db_integer/db_currency)和不同的轉換算法(表達式/static_cast).
讓我們用顯式模板特殊化機製來解決這個問題。我們得有一個FetchField﹐可以針對一個
模板類來產生不同的變體﹐而那個模板類又能夠針對int和double進行顯式特殊化。每個
特殊化都必須為這些變體提供統一的名稱。
// Example 4: Defining DbTraits
//
// Most general case not implemented  最通用的情況沒有實現
template <typename T> struct DbTraits;
// Specialization for int
template <>
struct DbTraits<int>
{
    enum { TypeId = DB_INTEGER };
    typedef db_integer DbNativeType;
    // 注意下面的Convert是static member function      譯者
    static void Convert(DbNativeType from, int& to)
    {
        to = static_cast<int>(from);
    }
};
// Specialization for double
template <>
struct DbTraits<double>
{
    enum { TypeId = DB_CURRENCY };
    typedef db_currency DbNativeType;
    // 注意下面的Convert是static member function      譯者
    static void Convert(const DbNativeType& from, double& to)
    {
        to = from.integral_part + from.fractionary_part / 100.;
    }
};
現在﹐如果你寫DbTraits<int>::TypeId﹐你得到的就是DB_INTEGER﹐而對於
DbTraits<double>::TypeId﹐得到的就是DB_CURRENCY﹐對於
DbTraits<anything_else>::TypeId﹐得到的是什麼呢﹖Compile-time error!
因為模板類本身只是聲明﹐並沒有定義。
是不是一勞永逸了﹖看看我們如何利用DbTraits來實現FetchField就放心了。
我們把所有變化的部份    枚舉類型  API類型  轉換算法    都放在了DbTraits
裡﹐這下我們的函數裡只包含FetchIntField和FetchDoubleField的相同部份了﹕
// Example 5: A generic, extensible FetchField using DbTraits
//
template <class T>
void FetchField(db_cursor& cr, unsigned int col, T& val)
{
    // Define the traits type
    typedef DbTraits<T> Traits;
    if (cr.column_type[col] != Traits::TypeId)
        throw std::runtime_error("Column type mismatch");
    if (!db_access_column(&cr, col))
        throw std::runtime_error("Cannot transfer data");
    typename Traits::DbNativeType temp;
    memcpy(&temp, cr.column_data[col], sizeof(temp));
    Traits::Convert(temp, val);
    db_release_column(&cr, col);
}
搞定了  我們只不過實現和使用了一個traits模板類而已  
Traits依靠顯式模板特殊化來把代碼中因類型不同而發生變化的片斷拖出來﹐用統一的
接口來包裝。這個接口可以包含一個C++類所能包含的任何東西﹕內嵌類型﹐成員函數﹐
成員變量﹐作為客戶的模板代碼可以通過traits模板類所公開的接口來間接訪問之。
這樣的traits接口通常是隱式的﹐隱式接口不如函數簽名(function signatures)那麼
嚴格﹐例如﹐儘管DbTraits<int>::Convert和DbTraits<double>::Convert有  非常不
同的簽名﹐但它們都可以正常工作。
Traits模板類在各種類型上建立一個統一的接口﹐而又針對各種類型提供不同的實現細節。
由於Traits抓住了一個概念﹐一個相關聯的選擇集﹐所以能夠在相似的contexts中被重用。
定義﹕ A traits template is a template class, possibly explicitly 
specialized, that provides a uniform symbolic interface over a coherent 
set of design choices that vary from one type to another. 
       
       Traits模板是一個模板類﹐很可能是顯式特殊化的模板類﹐它為一系列根據不同類
   型做出的設計選擇提供了一個統一的  符號化的接口。
TRAITS AS ADAPTERS: 用作適配子的TRAITS
數據庫的問題說得夠多了﹐現在我們來看一個更一般的例子    smart pointers。
假設你正在設計一個SmartPtr模板類。對於一個smart pointer來說﹐最美妙的事情是它們
可以自動管理內存問題﹐同時在其他方面又像一個常規指針。而不那麼美妙的事情是﹐它們
的實現代碼可不是好對付的﹐有些C++的smart pointer實現技術簡直就是在黑暗中變戲法。
這一殘酷的事實帶來了一個重要實踐經驗﹕你最好盡一切可能一勞永逸﹐寫出一個出色的  
具有工業強度的smart pointer來滿足你所有的需求。此外﹐你通常不能修改一個類來適應
你的smart pointer﹐所以你的SmartPtr一定要足夠靈活。
有不少類層次使用引用計數(reference counting)以及相應的函數管理對象的生存期。然而﹐
並沒有reference counting的標準實現方法﹐每一個C++庫的供應商在實現的語法和/或語義上
都有所不同。例如﹐在你的應用程序中有這樣兩個interfaces﹕
大部份的類實現了RefCounted接口: 
class RefCounted
{
public:
    void IncRef() = 0;
    bool DecRef() = 0; // if you DecRef() to zero
        // references, the object is destroyed
        // automatically and DecRef() returns true
    virtual ~RefCounted() {}
};
而由第三方提供的Widget類使用不同的接口﹕
class Widget
{
public:
    void AddReference();
    int RemoveReference(); // returns the remaining
        // number of references; it's the client's
        // responsibility to destroy the object
    ...
};
不過你並不想維護兩個smart pointer類﹐你想讓兩種類共享一個SmartPtr。一個基於traits的
解決方案把兩種不同的接口用語法和語義上統一的接口包裝起來﹐建立針對普通類的通用模板﹐
而針對Widget建立一個特殊化版本﹐如下﹕
// Example 6: Reference counting traits
//
template <class T>
class RefCountingTraits
{
    static void Refer(T* p)
    {
        p->IncRef(); // assume RefCounted interface
    }
    static void Unrefer(T* p)
    {
        p->DecRef(); // assume RefCounted interface
    }
};
template <>
class RefCountingTraits<Widget>
{
    static void Refer(Widget* p)
    {
        p->AddReference(); // use Widget interface
    }
    static void Unrefer(Widget* p)
    {
        // use Widget interface
        if (p->RemoveReference() == 0)
            delete p;
    }
};
在SmartPtr裡﹐我們像這樣使用RefCountingTraits:
template <class T>
class SmartPtr
{
private:
    typedef RefCountingTraits<T> RCTraits;
    T* pointee_;
public:
    ...
    ~SmartPtr()
    {
        RCTraits::Unrefer(pointee_);
    }
}﹔
當然在上面的例子裡﹐你可能會爭論說你可以直接特殊化Widget類的SmartPtr的構造與析構函數。
你可以使用把模板特殊化技術用在SmartPtr本身﹐而不是用在trait上頭﹐這樣還可以消除額外的
類。儘管對這個問題來說這種想法沒錯﹐但還是由一些你需要注意的缺陷﹕
[譯者為了說明起見﹐提供一個針對SmartPtr本身進行顯式特殊化的範例供作者批判:]
// 通用類型版
template <class T>
class SmartPtr {
private:
    T *pointee_;
public:
    SmartPtr(T* pobj) {
	pointee_ = pobj;
	pobj->IncRef();
    }
    ...
    ~SmartPtr() {
	pointee_->DecRef();
    }
};
// Widget類專用版
template <>
class SmartPtr<Widget> {
private:
    T *pointee_;
public:
    SmartPtr(T* pobj) {
	pointee_ = pobj;
	pobj->AddReference();
    }
    ...
    ~SmartPtr() {
	if (pointee_->RemoveReference() == 0)
	    delete pointee_;
    }
};
  •這麼干缺乏可擴展性。如果給給SmartPtr再增加一個模板三數﹐喏﹐全完蛋了  你不能特殊
    化這樣一個SmartPtr<T. U>成員函數﹐其中模板三數T是Widget﹐而U可以為其他任何類型。不﹐你做
    不到。附帶說一句﹐smart pointer經常用作模板三數。
[譯者附釋﹕作者說做不到的情形如下﹕
 template <class T, class U>
 class Demo {
 public:
	void dostuff() {...}  //Generic版本dostuff()
 };

 template<>
 void Demo<char, int>::dostuff() {...}  //這是可以的﹐針對成員函數進行specialize

 template <class U>
 void Demo<Widget, U>::dostuff() {...} // 這是不行的﹐編譯器會去尋找一個Demo<Widget, U>的
                                       // partial specialization defination, 由於並沒有這
				       // 麼一個defination﹐所以編譯失敗。
  •最終代碼不那麼清晰。Trait有一個名字﹐而且把相關的東西很好的組織起來﹐因此使用traits的
    代碼更加容易理解。相比之下﹐用直接特殊化SmartPtr成員函數的代碼﹐看上去更招黑客的喜歡。
  •你沒法對一種類型使用多種traits。
用繼承機製的解決方案﹐就算本身完美無瑕﹐也至少存在上述的缺陷。解決這樣一個變體問題﹐
使用繼承實在是太笨重了。此外﹐通常用以取代繼承方案的另一種經典機製    containment﹐
用在這裡也顯得畫蛇添足﹐繁瑣不堪。相反﹐traits方案乾淨利落﹐簡明有效﹐物合其用﹐恰到好處。
Traits的一個重要的應用是“interface glue”(接口膠合劑)﹐通用的  可適應性極強的
適配子。如果不同的類對於一個給定的概念有  不同的實現﹐traits可以把這些實現再組織統
一成一個公共的接口。
對於一個給定類型提供多種TRAITS﹕現在﹐我們假設所有的人都很喜歡你的SmartPtr模板類﹐
直到有一天﹐在你的多線程應用程序裡開始現了神秘的bug﹐美夢被驚醒了。你發現罪魁禍首
是Widget﹐它的引用計數函數並不是線程安全的。現在你不得不親自實現Widget::AddReference和
Widget::RemoveReference﹐最合理的位置應該是在RefCountingTraits中﹐打上個補丁吧﹕
// Example 7: Patching Widget's traits for thread safety
//
template <>
class RefCountingTraits<Widget>
{
    static void Refer(Widget* p)
    {
        Sentry s(lock_); // serialize access
        p->AddReference();
    }
    static void Unrefer(Widget* p)
    {
        Sentry s(lock_); // serialize access
        if (p->RemoveReference() == 0)
            delete p;
    }
private:
    static Lock lock_;
};
不幸的是﹐雖然你重新編譯  測試之後正確運行﹐但是程序慢得像蝸牛。仔細分析之後發現﹐你
剛纔的所作所為往程序裡塞了一個糟糕的瓶頸。實際上只有少數幾個Widget是需要能夠被好幾個
線程訪問的﹐余下的絕大多數Widget都是只被一個線程訪問的。
你要做的是告訴編譯器按你的需求分別使用多線程traits和單線程traits這兩個不同版本。
你的代碼主要使用單線程traits.
如何告訴編譯器使用那個traits﹖這麼干﹕把traits作為另一個模板三數傳給SmartPtr。缺省
情況下傳遞老式的traits模板﹐而用特定的類型實例化特定的模板。
template <class T, class RCTraits = RefCountingTraits<T> >
class SmartPtr
{
    ...
};
你對單線程版的RefCountingTraits<Widget>不做改動﹐而把多線程版放在一個單獨的類中﹕
class MtRefCountingTraits
{
    static void Refer(Widget* p)
    {
        Sentry s(lock_); // serialize access
        p->AddReference();
    }
    static void Unrefer(Widget* p)
    {
        Sentry s(lock_); // serialize access
        if (p->RemoveReference() == 0)
            delete p;
    }
private:
    static Lock lock_;
};
現在你可將SmartPtr<Widget>用于單線程目的﹐將SmartPtr<Widget, MtRefCountingTraits>
用于多線程目的。這就是了  就跟Scott Meyers可能會說的那樣﹐“你要是沒體會過快樂﹐
就不知道怎麼找樂。”
如果一種類型只要一個trait由可以應付﹐那麼只要用顯式模板特殊化就夠了。現在即使一個類型
需要多個trait來應付我們也搞得定。所以﹐traits必須能夠從外界塞進來﹐而不是在內部“算出來”。
一個應當謹記的慣用法是提供一個traits類作為最後一個模板三數。缺省的traits通過模板三數缺
省值給定。
定義﹕一個traits類(與traits模板類相對)或者是一個traits模板類的實例﹐或者是一個與traits
模板類展現出相同接口的單獨的類。
(完)
Andrei Alexandrescu在位於華盛頓州西雅圖市的RealNetworks公司中任開發經理。