2016年11月30日 星期三

C++ 沉思錄: 遲來的較大嬰兒奶粉

C++ 進階讀物裡,最有名的大概就是 Scott Meyers 寫的那本 Effective C++,光是前公司辦公室裡大概就有 5、6 本。小弟也翻閱了很多次。

Effective C++ 比較偏向原則性的說明,什麼該做,什麼不該做,什麼做了必死無疑。書中的範例都很片段,總有些讓人意猶未盡。

今天推薦的這本 C++ 沉思錄,改從另外一種角度出發,先從一個很小的雛型開始,逐步添枝加葉,過程中告訴你為什麼要這樣做,會面臨哪些設計決策,最後完成一個完整的範例。

這樣的寫作方式,小弟一讀就上癮了,因為這更貼近程式員的實際工作狀況:「寫出一些 classes,拼湊他們,卡住了,回到上一步...」

作者 Andrew Koenig、 Barbara Moo 是 C++ 元老級人物,後者與 Stanley Lippman 寫了很有名的教科書 C++ Primer(印象中 Moo 為 Lippman  的研究所導師),Andrew Koenig  更是被 Scott Meyers 列為 C++ 史上影響力前五大人物,本書可看性可想而知。

以下就來談談這本書我認為最精華的部份


參考計數(Reference Counting)

在 C++ 之後流行的 OOPL,幾乎都提供垃圾收集機制,C++ 缺少此功能,同樣的程式與 Java 比起來就是又長又彆扭,因為 C++ 程式員總是要很小心的避免 memory leak。

參考計數可以說是 C++ 人工版垃圾收集,作者很強調這個技巧,本書第 6 章-10章都是此技巧的展現,在他的另外一本著作 Accelerated C++ 裡也提到了這個技巧。

這個技巧的核心精神就是不直接使用 class 產生物件,而是另外設計一個 handle class,這個 handle class 用一個指標指向前者 class 產生的物件。



這樣做有何好處?脫褲子放屁?其中的關鍵有兩個:

記憶體自動管理

如下圖所示,利用藏於 Foo 中的計數器,FooHandle destructor 可以檢查是否為最後一個使用者,如果是就把物件刪除:


當然在 2016 年的今天,我們已經有了 boost::shared_ptr,而應用此技巧最兇最有名的 framework 就是大名鼎鼎的 Qt,不過 Qt 的作法與 shared_ptr 還是有所差異的,例如 Qt 中最常用的 QString 修改字串時採取的是 copy-on-write 策略,一旦字串被修改,就會配置記憶體產生新字串,shared_ptr 無論怎樣讀寫物件都不會產生新物件出來,本書兩種作法都有提供範本。

Handle 與 shared_ptr 還有另外一個重大差異,以上圖來說,以 handle class 產生的物件在誕生時內部指標就會配置物件,以上圖來說 FooHandle fh 就會使得 FooHandle::f_ = new Foo,而 shared_ptr 並沒有此種行為,這是合理的,因為指標的語意不包含自動配置記憶體這一塊。

保留物件的結構資訊

當一個物件為複合物件,例如 Ch7-8 的加減乘除、Ch9-10 的字元繪圖,要表達這些樹狀結構,沒有用到指標是不可能的,用到指標就需要面對記憶體管理。弄到最後...好像也沒有比 C 輕鬆,很多人此時就會面臨鬼打牆的狀況,有些人就乾脆回頭用 C 了。

還有一派人的作法是用 std::map、std::list 來形塑自己需要的樹狀結構,這樣做程式為了有時候弄的削足適履,而且 std::map、std::list 也沒辦法刪除其中的節點後自動刪除節點指向的物件(除非配合 shared_ptr 這種具備參照計數能力的 smart pointer),刪除節點後也要小心 iterator 的失效問題。

所以 handle class 還是非常有用的技法,除了可以免除用大砲轟小鳥(+stl, boost),可以寫出更精緻的程式,也不用在程式中勉力記住何時該 new ,何時該 delete。甚至也有 smart pointer 不可取代的部份,此時可以把物件視為一般的數值傳遞,配合適當的 operator overloading 可以達成我們需要的物件複合運算。如果使用 smart pointer,為了配合指標的語意,就只能使用 *xx + *yy 這樣的語法。

參考計數的缺點

看來有了 handle 技法後我們可以寫 C++ 有如寫 Python、Ruby 一樣爽快了,但這招還是有其極限,當碰上環狀結構時有可能發生 memory leak,下面是一個範例:



//foo_handle.h
#ifndef FOO_HANDLE_H
#define FOO_HANDLE_H

class Foo;
class BarHandle;

class FooHandle {
    Foo *f_;
    
public:    
    FooHandle(Foo *f);
    FooHandle();
    ~FooHandle();
    FooHandle(const FooHandle& src);
    FooHandle& operator=(const FooHandle& src);
    void set(const BarHandle& b);
};

#endif

//foo_handle.cpp
#include "foo.h"
#include "foo_handle.h"

FooHandle::FooHandle(Foo *f):f_(f)
{    
}

FooHandle::FooHandle():
    f_(0)
{
}    


FooHandle::~FooHandle()
{
    if(f_ != 0 && --f_->use_ == 0)
        delete f_;
}

FooHandle::FooHandle(const FooHandle& src):f_(src.f_)
{
    ++src.f_->use_;
}

FooHandle& FooHandle::operator=(const FooHandle& src)
{
    ++src.f_->use_;
    if(f_ != 0 && --f_->use_ == 0)
        delete f_;
    
    f_ = src.f_;
    return *this;
}


void FooHandle::set(const BarHandle& b)
{
    if(f_)
        f_->set(b);
}
//foo.h
#ifndef FOO_H
#define FOO_H

#include "bar_handle.h"

class Foo {
    friend class FooHandle;
    int use_;
    BarHandle b_;
public:
    void set(const BarHandle& b);
    Foo();
    ~Foo();
    
};


#endif
//foo.cpp
#include "foo.h"

Foo::Foo():
    use_(1)
{
}    

Foo::~Foo()
{    
}

void Foo::set(const BarHandle& b)
{
    b_ = b;
}
//bar_handle.h
#ifndef BAR_HANDLE_H
#define BAR_HANDLE_H

class Bar;
class FooHandle;
class BarHandle {
    Bar *b_;
    
public:
    BarHandle(Bar *b);
    BarHandle();
    ~BarHandle();
    BarHandle(const BarHandle& src);
    BarHandle& operator=(const BarHandle& src);
    void set(const FooHandle& f);
};

#endif
//bar_handle.cpp
#include "bar.h"
#include "bar_handle.h"

BarHandle::BarHandle(Bar *b):b_(b)
{    
}

BarHandle::BarHandle():
    b_(0)
{
}    

BarHandle::~BarHandle()
{
    if(b_ != 0 && --b_->use_ == 0)
        delete b_;
}

BarHandle::BarHandle(const BarHandle& src):b_(src.b_)
{
    ++src.b_->use_;
}

BarHandle& BarHandle::operator=(const BarHandle& src)
{
    ++src.b_->use_;
    if(b_ != 0 && --b_->use_ == 0)
        delete b_;
    
    b_ = src.b_;
    return *this;
}

void BarHandle::set(const FooHandle& f)
{
    if(b_)
        b_->set(f);
}
//bar.h
#ifndef BAR_H
#define BAR_H

#include "foo_handle.h"

class Bar {
    friend class BarHandle;
    int use_;
    FooHandle f_;
    
public:
    void set(const FooHandle& f);

    Bar();
    ~Bar();    
};


#endif
//bar.cpp
#include "bar.h"

Bar::Bar():
    use_(1)
{
}    

Bar::~Bar()
{    
}

void Bar::set(const FooHandle& f)
{
    f_ = f;
}
#include "bar_handle.h"
#include "foo_handle.h"
#include "foo.h"
#include "bar.h"

int main(int argc, char *argv[])
{
    FooHandle f(new Foo);
    BarHandle b(new Bar);
    f.set(b);
    b.set(f);
    return 0;
}

用 valgrind 檢查,沒證據不能亂說話:


的確發生了 memory leak,這個問題即使使用 shared_ptr 也會發生,需要另外搭配 weak_ptr,看來寫 C++ 永遠是件苦差事?

結語

其實看了 Ch1-Ch10,作者使用的技巧只是枝微末結,他真正厲害的地方是對問題的分析,總是能抓到問題的本質,此種「分析」的能力才是促使他晉升 C++ 名人堂的真正原因,雖然本書已有相當年紀,但仍十分具有啟發性,強力推薦!

4 則留言:

  1. 回覆
    1. 回頭翻了一下已經放到生菇的 More Effective C++, 也有提到這個技巧, 但沉思錄比較淺顯易懂, Scott Meyers 的技巧較為完整華麗

      刪除
  2. std::string的Copy-on-Write:不如想象中美好
    http://www.cnblogs.com/promise6522/archive/2012/03/22/2412686.html
    有 thread safe 的問題

    回覆刪除
    回覆
    1. Qt 也有同樣問題,不過我的原則就是盡量不用thread

      刪除