【轉】Boost和loki智能指針

原帖: http://dozb.bokee.com/1976635.html

一、 Boost 智能指針
Boost 的智能指針方案實現了五種智能指針模板類,每種智能指針都用於不同的目的。這五種智能指針是:

  template<typename T> class scoped_ptr;
  template<typename T> class scoped_array;
  template<typename T> class shared_ptr;
  template<typename T> class shared_array;
  template<typename T> class weak_ptr;


下面將分別介紹它們各自的特性,並給出相應的使用實例:


  scoped_ptr:意在用作指向自動(棧)對象的、不可複製的智能指針。該模板類存儲的是指向動態分配的對象(通過new 分配)的指針。被指向的對象保證會被刪除,或是在scoped_ptr 析構時,或是通過顯式地調用reset 方法。注意該模板沒有“共享所有權”或是“所有權轉讓”語義。同時,它也是不可複製的(noncopyable)。正因爲如此,在用於不應被複制的指針時,它比shared_ptr 或std:auto_ptr 要更安全。與auto_ptr一樣,scoped_ptr 也不能用於STL 容器中;要滿足這樣的需求,應該使用shared_ptr。另外,它也不能用於存儲指向動態分配的數組的指針,這樣的情況應使用scoped_array。

下面是使用scoped_ptr 的一個簡單實例:

class CTest
{
public:
    CTest() : m_id(0) {}
    CTest(int id) : m_id(id) {}
    ~CTest() { std::cout << "id: " << m_id << " - Destructor is being called/n"; }
    void SetId(int id) { m_id = id; }
    int GetId() { return m_id; }
    void DoSomething()
    { std::cout << "id: " << m_id << " - Doing something/n"; }
private:
    int m_id;
};
void main()
{
    boost::scoped_ptr<CTest> pTest(new CTest);
    pTest->DoSomething();
}


其運行結果爲:
  id: 0 - Doing something
  id: 0 - Destructor is being called
(以下的幾個例子所用的CTest 類的定義完全相同,爲節省篇幅,不再列出——作者)

顯然,儘管我們自己沒有調用delete,pTest 仍然爲我們正確地刪除了它所指向的對象。看起來scoped_ptr的用途和auto_ptr 十分類似,但實際上,scoped_ptr 類型的指針的所有權不可轉讓,這一點是和auto_ptr相當不同的。


  scoped_array:該模板類與scoped_ptr 類似,但意在用於數組而不是單個對象。std::vector 可用於替換scoped_array,並且遠爲靈活,但其效率要低一點。在不使用動態分配時,boost::array 也可用於替換scoped_array。

下面是一個使用scoped_array 的實例:

void main()
{
    boost::scoped_array<CTest> pTest(new CTest[2]);
    pTest[0].SetId(0);
    pTest[1].SetId(1);
    pTest[0].DoSomething();
    pTest[1].DoSomething();
   
std::cout << '/n';
}


其運行結果爲:
  id: 0 - Doing something
  id: 1 - Doing something
  id: 1 - Destructor is being called
  id: 0 - Destructor is being called
scoped_array 將負責使用delete [],而不是delete 來刪除它所指向的對象。


  shared_ptr:意在用於對被指向對象的所有權進行共享。與scoped_ptr 一樣,被指向對象也保證會被刪除,但不同的是,這將發生在最後一個指向它的shared_ptr 被銷燬時,或是調用reset 方法時。shared_ptr符合C++標準庫的“可複製構造”(CopyConstructible)和“可賦值”(Assignable)要求,所以可被用於標
準的庫容器中。另外它還提供了比較操作符,所以可與標準庫的關聯容器一起工作。shared_ptr 不能用於存儲指向動態分配的數組的指針,這樣的情況應該使用shared_array。該模板的實現採用了引用計數技術,所以無法正確處理循環引用的情況。可以使用weak_ptr 來“打破循環”。shared_ptr 還可在多線程環境中使用。

下面的例子演示怎樣將shared_ptr 用於std::vector 中:

typedef boost::shared_ptr<CTest> TestPtr;
void PT(const TestPtr &t)
{
    std::cout << "id: " << t->GetId() << "/t/t" << "use count: " << t.use_count() << '/n';
}
void main()
{
    std::vector<TestPtr> TestVector;
    TestPtr pTest0(new CTest(0));
    TestVector.push_back(pTest0);
    TestPtr pTest1(new CTest(1));
    TestVector.push_back(pTest1);
    TestPtr pTest2(new CTest(2));
    TestVector.push_back(pTest2);
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '/n';
    pTest0.reset();
    pTest1.reset();
    pTest2.reset();
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '/n';
    TestVector.clear();
    std::cout << '/n';
    std::cout << "exiting.../n";
}


其運行結果爲:
  id: 0 use count: 2
  id: 1 use count: 2
  id: 2 use count: 2
  id: 0 use count: 1
  id: 1 use count: 1
  id: 2 use count: 1
  id: 0 - Destructor is being called
  id: 1 - Destructor is being called
  id: 2 - Destructor is being called
  exiting...

運行結果中的“use count”是通過shared_ptr 的use_count()方法獲得的“使用計數”,也就是,對所存儲指針進行共享的shared_ptr 對象的數目。我們可以看到,在通過new 分配了3 個CTest 對象,並將相應的shared_ptr 對象放入TestVector 後,三個使用計數都爲2;而在我們使用reset()方法復位pTest0、pTest1 和pTest2 後,TestVector 中的各個shared_ptr 對象的使用計數變成了1。這時,我們調用TestVector的clear()方法清除它所包含的shared_ptr 對象;因爲已經沒有shared_ptr 對象再指向我們先前分配的3個CTest 對象,這3 個對象也隨之被刪除,並導致相應的析構器被調用。


  shared_array:該模板類與shared_ptr 類似,但意在用於數組而不是單個對象。指向std::vector 的shared_ptr 可用於替換scoped_array,並且遠爲靈活,但其效率也要低一點。

下面是使用實例:

void main()
{
    boost::shared_array<CTest> pTest1(new CTest[2]);
    pTest1[0].SetId(0);
    pTest1[1].SetId(1);
    std::cout << "use count: " << pTest1.use_count() << "/n/n";
    boost::shared_array<CTest> pTest2(pTest1);
    std::cout << "use count: " << pTest1.use_count() << "/n/n";
    pTest1.reset();
    pTest2[0].DoSomething();
    pTest2[1].DoSomething();
    std::cout << '/n';
    std::cout << "use count: " << pTest1.use_count() << "/n/n";
}


其運行結果爲:
  use count: 1
  use count: 2
  id: 0 - Doing something
  id: 1 - Doing something
  use count: 1
  id: 1 - Destructor is being called
  id: 0 - Destructor is being called
如此例所示,我們通過new 所分配的數組只有在指向它的pTest1 和pTest2 都被銷燬或復位後才被刪除。


  weak_ptr:該模板類存儲“已由shared_ptr 管理的對象”的“弱引用”。要訪問weak_ptr 所指向的對象,可以使用shared_ptr 構造器或make_shared 函數來將weak_ptr 轉換爲shared_ptr。指向被管理對象的最後一個shared_ptr 被銷燬時將刪除該對象,即使仍有weak_ptr 指向它也是如此。與原始指針不同的是,屆時最後一個shared_ptr 會檢查是否有weak_ptr 指向該對象,如果有的話就將這些weak_ptr 置爲空。這樣就不會發生使用原始指針時可能出現的“懸吊指針”(dangling pointer)情況,從而獲得更高的安全水平。
  weak_ptr 符合C++標準庫的“可複製構造”(CopyConstructible)和“可賦值”(Assignable)要求,所以可被用於標準的庫容器中。另外它還提供了比較操作符,所以可與標準庫的關聯容器一起工作。

void main()
{
    boost::shared_ptr<CTest> pTest(new CTest);
    boost::weak_ptr<CTest> pTest2(pTest);
    if(boost::shared_ptr<CTest> pTest3 = boost::make_shared(pTest2))
        pTest3->DoSomething();
    pTest.reset();
    assert(pTest2.get() == NULL);
}


其運行結果爲:
  id: 0 - Doing something
  id: 0 - Destructor is being called
main 函數最後的斷言確認了pTest2 所存儲的指針的確已被置爲NULL。

  顯然,Boost 的智能指針方案會讓我們產生這樣的疑問:如果我們還需要其他類型的智能指針(比如支持COM 的智能指針),是否意味着我們必須在C++中再增加智能指針類型,或是採用非標準的實現呢?在泛型技術已得到極大發展的今天,Boost 的“增加增加再增加”的思路是不能讓人滿意的。正是在這裏,我們看到了下面將要介紹的Loki Smart Pointer 的關鍵點:通過基於策略(policy-based)的設計來實現通用的智能指針模板。


二、 Loki 智能指針
  按照美國傳統辭典(雙解)的解釋,Loki 是“A Norse god who created discord, especially among his fellow gods.”(斯堪的納維亞的一個製造混亂的神,尤其是在其同類之間)。就其給Boost 智能指針帶來的麻煩而言,Loki 智能指針倒真的當得起這個名字;而在另一方面,就其實現的優雅以及功能的強大(也就是說,它給開發者帶來的好處)而言,它也的確屬於“神族”。
  上面已經說過,Loki 的智能指針方案採用了基於策略的設計。其要點在於把將各功能域分解爲獨立的、由主模板類進行混合和搭配的策略。讓我們先來看一看Loki 智能指針模板類SmartPtr 的定義:

template
<
    typename T,
    template <class>class OwnershipPolicy =RefCounted,
    class ConversionPolicy =DisallowConversion,
    template <class>class CheckingPolicy =AssertCheck,
    template <class>class StoragePolicy =DefaultSPStorage
>
class SmartPtr;


我們可以看到,除了SmartPtr 所指向的對象類型T 以外,在模板類SmartPtr 中包括了這樣一些策略:OwnershipPolicy(所有權策略)、ConversionPolicy(類型轉換策略)、CheckingPolicy(檢查策略)、StoragePolicy(存儲策略)。正是通過這樣的分解,使得SmartPtr 具備了極大的靈活性。我們可以任意組合各種不同的策略,從而獲得不同的智能指針實現。下面先對各個策略逐一進行介紹:

  OwnershipPolicy:指定所有權管理策略,可以從以下預定義的策略中選擇:DeepCopy(深度複製)、RefCounted(引用計數)、RefCountedMT(多線程化引用計數)、COMRefCounted(COM 引用計數)、RefLinked(引用鏈接)、DestructiveCopy(銷燬式複製),以及NoCopy(無複製)。
  ConversionPolicy:指定是否允許進行向被指向類型的隱式轉換。可以使用的實現有AllowConversion 和DisallowConversion。
  CheckingPolicy:定義錯誤檢查策略。可以使用AssertCheck、AssertCheckStrict、RejectNullStatic、RejectNull、RejectNullStrict,以及NoCheck。
  StoragePolicy:定義怎樣存儲和訪問被指向對象。Loki 已定義的策略有:DefaultSPStorage、ArrayStorage、LockedStorage,以及HeapStorage。

除了Loki 已經定義的策略,你還可以自行定義策略。實際上,Loki 的智能指針模板覆蓋了四種基本的Boost 智能指針類型:scoped_ptr、scoped_array、shared_ptr 和shared_array;至於weak_ptr,也可以通過定義相應的策略來實現其等價物。通過即將成爲C++標準(C++0x)的typedef 模板特性,我們還可以利用Loki 的SmartPtr 模板來直接定義前面提到的Boost 的前四種智能指針類型。舉例來說,我們可以這樣定義:

shared_ptr:
template<typename T> // typedef 模板還不是標準的
typedef Loki::SmartPtr
<
    T,
    RefCounted, // 以下都是缺省的模板參數
    DisallowConversion,
    AssertCheck,
    DefaultSPStorage
>
shared_ptr;


下面是一個使用Loki “shared_ptr”的實例:

typedef Loki::SmartPtr<CTest> TestPtr;
void PT(const TestPtr &t)
{
    std::cout << "id: " << t->GetId() << '/n';
}

void main()
{
    std::vector<TestPtr> TestVector;
    TestPtr pTest0(new CTest(0));
    TestVector.push_back(pTest0);
    TestPtr pTest1(new CTest(1));
    TestVector.push_back(pTest1);
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '/n';
    Loki::Reset(pTest0, NULL);
    Loki::Reset(pTest1, NULL);
    std::for_each(TestVector.begin(), TestVector.end(), PT);
    std::cout << '/n';
    TestVector.clear();
    std::cout << '/n';
    std::cout << "exiting.../n";
}


其運行結果爲:
  id: 0
  id: 1
  id: 0
  id: 1
  id: 0 - Destructor is being called
  id: 1 - Destructor is being called
  exiting...

前面已經提到,要通過Loki 定義與Boost 的shared_ptr 功能等價的智能指針,除了第一個模板參數以外,其他的參數都可以使用缺省值,所以在上面的例子中,我們直接使用“typedef Loki::SmartPtr<CTest> TestPtr;”就可以了。非常的簡單!

爲了進一步說明Loki 的“基於策略的設計方法”,讓我們再來看一個更爲複雜的例子:通過Loki::SmartPtr 實現線程專有存儲(Thread-Specific Storage,TSS;又稱線程局部存儲,Thread Local Storage,TLS)。

所謂線程專有存儲,是指這樣一種機制,通過它,多線程程序可以使用一個邏輯上的全局訪問點來訪問線程專有的數據,並且不會給每次訪問增加額外的鎖定開銷。舉一個簡單的例子,在C 語言中,我們可以通過errno變量來獲取錯誤代碼;通常errno 就是一個普通的全局變量——在單線程環境中,這當然沒有什麼問題,但如果
是多線程環境,這個全局的errno 變量就會給我們帶來麻煩了。TSS 正是解決這一問題的有效方案。

顯然,智能指針的語義能夠很好地適用於TSS。我們可以編寫一種智能指針,使得所有對其所指向對象的訪問都成爲線程專有的——也就是說,每個線程訪問的實際上是自己專有的對象,但從程序的外表來看,卻都是對同一對象的訪問。有了Loki::SmartPtr,我們可以非常容易地實現這樣的智能指針:如其名字所指示的,TSS 涉及的是存儲問題,我們只要對Loki::SmartPtr 的StoragePolicy 進行定製就可以了,其他的工作可以交給Loki::SmartPtr 去完成。

在POSIX PThreads 庫和Win32 中都提供了用於線程專有存儲的函數,它們分別是pthread_key_create、pthread_setspecific、pthread_getspecific 和pthread_key_delete(POSIX PThreads 庫),以及TlsAlloc、TlsSetvalue、TlsGetvalue 和TlsFree(Win32)。關於這些函數的詳細信息,請參閱相關的文檔。

下面給出在MSVC 6.0 下實現的用於TSS 的StoragePolicy,並通過註釋逐行進行分析(這個實現使用了PThreads-Win32 庫,這是一個Win32 上的PThreads 實現。使用Win32 的線程專有函數也可以實現類似的StoragePolicy,但編寫在線程退出時調用的CleanupHook()卻需要一點“竅門”。具體方法可參考Boost 的thread_specific_ptr 實現):

namespace Loki
{
    // 實現TSS 的Loki 存儲策略。改編自Douglas C. Schmidt、Timothy H. Harrison
    // 和Nat Pryce 的論文“Thread-Specific Storage for C/C++”中的部分代碼。
    // 使用了“Loki VC 6.0 Port”。
template <class T> class TS_SPStorage
{
public:
    typedef T* StoredType; // 被指向對象的類型。
    typedef T* PointerType; // operator->所返回的類型。
    typedef T& ReferenceType; // operator*所返回的類型。
public:
    // 構造器。對成員變量進行初始化。
    TS_SPStorage() : once_(0), key_(0), keylock_(NULL)
        { pthread_mutex_init(&keylock_, NULL); }
    // 析構器。釋放先前獲取的資源。
    ~TS_SPStorage()
    {
        pthread_mutex_destroy(&keylock_);
        pthread_key_delete(key_);
    }
    // 返回線程專有的被指向對象。
    PointerType operator->() const { return GetPointee(); }
    // 返回線程專有的被指向對象的引用。
    ReferenceType operator*() { return *GetPointee(); }
    // Accessors。獲取線程專有的被指向對象。
    friend inline PointerType GetImpl(TS_SPStorage& sp)
    { return sp.GetPointee(); }
    // 獲取線程專有的被指向對象的引用。
    // 該函數沒有實現。但NoCheck 需要它才能正常工作。
    friend inline const StoredType& GetImplRef(const TS_SPStorage& sp)
    { return 0; }
protected:
    // 銷燬所存儲的數據。空函數。該工作將由CleanupHook()在各個線程退出時完成。
    void Destroy() {}
    // 獲取當前線程專有的數據。
    PointerType GetPointee()
    {
        PointerType tss_data = NULL;
        // 使用雙重檢查鎖定模式來在除了初始化以外的情況下避免鎖定。
        // 之所以在這裏,而不是在構造器中對“專有鑰”進行初始化及分配TS 對象,
        // 是因爲:(1) 最初創建TS 對象的線程(例如,主線程)常常並不是使用
        // 它的線程(工作線程),所以在構造器中分配一個TS 對象的實例常常並無
        // 好處,因爲這個實例只能在主線程中訪問。(2)在有些平臺上,“專有
        // 鑰”是有限的資源,所以等到對TS 對象進行第一次訪問時再進行分配,有
        // 助於節省資源。
        // 第一次檢查。
        if(once_ == 0)
        {
            // 加鎖。確保訪問的序列化。
            pthread_mutex_lock(&keylock_);
            // 雙重檢查。
            if(once_ == 0)
            {
                // 創建“專有鑰”。
                pthread_key_create(&key_, &CleanupHook);
                // 必須在創建過程的最後出現,這樣才能防止其他線程在“專有鑰”
                // 被創建之前使用它。
                once_ = 1;
            }
            // 解鎖。
            pthread_mutex_unlock(&keylock_);
        }
        // 從系統的線程專有存儲中獲取數據。注意這裏不需要加鎖。
        tss_data = (PointerType)pthread_getspecific(key_);
        // 檢查是否這是當前線程第一次進行訪問。
        if (tss_data == NULL)
        {
            // 從堆中爲TS 對象分配內存。
            tss_data = new T;
            // 將其指針存儲在系統的線程專有存儲中。
            pthread_setspecific(key_, (void *)tss_data);
        }
        return tss_data;
    }
private:
    // 用於線程專有數據的“專有鑰”。
    pthread_key_t key_;
    // “第一次進入”標誌。
    int once_;
    // 用於避免在初始化過程中產生競態情況。
    pthread_mutex_t keylock_;
    // 清掃掛鉤函數,釋放爲TS 對象分配的內存。在每個線程退出時被調用。
    static void CleanupHook (void *ptr)
    {
        // 這裏必須進行類型轉換,相應的析構器纔會被調用(如果有的話)
        delete (PointerType)ptr;
    }
};

// 用於模擬typedef template 的結構。
// 參見Herb Sutter 的“Template Typedef”一文。
struct TS_SPStorageWrapper
{
    template <class T>
    struct In
    {
        typedef TS_SPStorage<T> type;
    };
};
};


下面讓我們來看一個使用實例。首先讓我們先定義:

Loki::SmartPtr
<
    int,
    Loki::NoCopyWrapper,
    Loki::DisallowConversion,
    Loki::NoCheckWrapper,
    Loki::TS_SPStorageWrapper
> value;


其含義爲:定義一種智能指針,被它指向的類型是int,OwnershipPolicy 是NoCopy,ConversionPolicy是DisallowConversion,CheckingPolicy 是NoCheck(因爲TS 對象存儲方式的限制,這是惟一能和TS_SPStorage 一起使用的CheckingPolicy。讀者可自行嘗試找到更好的解決方案),StoragePolicy 是TS_SPStorage。
然後,編寫這樣一個程序:

pthread_mutex_t io_mutex = NULL; // iostreams 不一定是線程安全的!
void *thread_proc(void *param)
{
    int id = (int)param;
    *value = 0;
    for (int i = 0; i < 3; i++)
    {
        (*value)++;
        pthread_mutex_lock(&io_mutex);
        std::cout << "thread " << id << ": " << *value << '/n';
        pthread_mutex_unlock(&io_mutex);
    }
    return NULL;
}

void main(int argc, char* argv[])
{
    pthread_mutex_init(&io_mutex, NULL);
    pthread_t id[3];
    for(int i = 0; i < 3; i++)
        pthread_create(&id[i], 0, thread_proc, (void *)i);
    for(int i = 0; i < 3; i++)
        pthread_join(id[i], NULL);
    pthread_mutex_destroy(&io_mutex);
}


其運行結果爲:
  thread 0: 1
  thread 0: 2
  thread 1: 1
  thread 2: 1
  thread 1: 2
  thread 2: 2
  thread 1: 3
  thread 2: 3
  thread 0: 3

由此我們可以看出,儘管看起來在各個線程中訪問的都是同樣的*value,但實際上訪問的卻是各自的線程專有的對象。而且除了初始化以外,對這些對象的訪問無需進行序列化。因爲Loki::SmartPtr 爲我們做了大量的工作,TS_SPStorage 的實現十分簡潔明瞭。有興趣的讀者,可以對這裏的實現與Boost 的thread_specific_ptr 進行比較。像這樣對Loki::SmartPtr 的策略進行的定製,在理論上數目是無限的,也就是說,通過它我們可以擁有五花八門、“千奇百怪”的智能指針——了不起的Loki,不是嗎?有了這樣的模板類,我們就再不需要一次一次地爲標準C++增加新的智能指針類型了。


三、 結束語
  在推進器Boost 和斯堪的納維亞之神Loki 的戰爭中,誰將勝出恐怕已不言而喻。倘若扮演上帝的C++標準委員會起了偏心,硬要選中Boost 的智能指針方案的話,那麼在接下來的日子裏,他們就將不再是C++標準委員會,而是C++智能指針救火委員會了。而即便那樣,Loki,斯堪的納維亞之神,也仍將矗立在C++大陸上,爲他的“子民”——C++程序員們——帶來統一、秩序和和諧。

相關資源
1. Herb Sutter,The New C++: Smart(er) Pointers,見C++ User Journal 網站:http://www.cuj.com/experts/2008/sutter.htm
2. Boost 文檔:http://www.boost.org
3. Andrei Alexandrescu,Modern C++ Design 及Loki,見http://www.moderncppdesign.com或http://www.awprofessional.com/catalog/product.asp?product_id=%7B4ED3E6F3-371F-4ADC-9810-CC7B936164E3%7D。
4. Douglas C. Schmidt、Timothy H. Harrison 和Nat Pryce,Thread-Specific Storage for C/C++,見http://www.cs.wustl.edu/~schmidt/或http://www.flyingd&#111nkey.com/ace/(中文)。
5. Herb Sutter,Template Typedef,見http://www.gotw.ca/gotw/079.htm。
6. PThreads-Win32 庫,見http://sources.redhat.com/pthreads-win32/。

<script src="http://yiluda.net/counter/counter.php?uid=5341&style=23&length=6"></script>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章