八、Customizing new and delete
對比於 Java 和 .NET 的內建「垃圾回收能力」,C++ 對記憶體管理的純手工法看起來有點老氣,但許多苛刻的系統程式開發人員之所以選擇 C++,就是因為它允許手工管理記憶體,這樣的開發人員研究並學習軟體使用記憶體的行為特徵,然後修改配置和歸還工作,以求獲得其所建置的系統最佳效率。

這麼做的前提是,瞭解 C++ 記憶體管理常式的行為,兩個主角是 allocation and deallocation routines,也就是 operator new 和 operator delete,配角是 new-handler,這是當 operator new 無法滿足的記憶體需求的所喚起的 function。

multi-thread 的記憶體管理下,由於 heap 是一個可被改動的全域性資源,因此充斥著發狂存取這一類資源的 race condition 出現機會,本章中多個條款提及使用可改動之 static 資料,這總是會令 thread-aware programmer 高度警戒如坐針氈,因為如果沒有適當的 synchronization,一旦使用 lock-free algorithm,或沒有精心防止 concurrent access 時,呼叫記憶體常式可能很容易導致管理 heap 的資料結構內容敗壞。

另外,operator new 和 operator delete 只適合用來配置單一物件, Arrays 所用的記憶體由 operator new[] 配置出來,並由 operator delete[] 歸還,而除非特別表示,否則本章中每件關於 operator new 和 operator delete 的事,也都適用於 operator new[] 和 operator delete[]。


49、Understand the behavior of the new-handler.
當 operator new 無法滿足某一記憶體配置需求時,它會拋出 exception,以前它會傳回 NULL pointer,而某些舊式的 compiler 目前也還這麼做。

當 operator new 拋出 exception 以反應一個未獲滿足的記憶體需求之前,會先呼叫一個客戶指定的錯誤處理函式,一個所謂的 new-handler (詳見條款 51),為了指定這個「用以處理記憶體不足」的 function,使用者必須呼叫 set_new_handler,這是宣告於 <new> 的一個 standard library function:
namespace std {
    typedef void (*new_handler) ( );
    new_handler set_new_handler(new_handler p) throw();
}

new_handler 是個 typedef,定義出一個 pointer 指向 function,該 function 沒有參數也不傳回任何東西,set_new_handler 則是「獲得一個 new_handler 並傳回一個 new_handler」的 function,set_new_handler 宣告式尾端的 "throw()" 表示該 function 不能拋出任何 exception,詳見條款 29

set_new_handler 的參數是個 pointer,指向 operator new 無法配置足夠記憶體時該被喚起的 function,而反回值也是個 pointer,指向被呼叫前正在執行但馬上就要被替換的那個 new-handler function。
void outOfMem() {
    std::cerr << "Unable to satisfy request for memory\n";
    std::abort();
}

int main() {
    std::set_new_handler(outOfMem);
    int* pBigDataArray = new int[100000000L];
    ...
}

就上例而言,如果 operator new 無法配置足夠的空間, outOfMem 就會被喚起,於是程式發出一個訊息之後夭折(abort)。

當 operator new 無法滿足記憶體申請時,它會不斷呼叫 new-handler function,直到找到足夠記憶體,引起反覆呼叫的程式碼須參考條款 51,一個設計良好的 new-handler function 必須做到:
  • 讓更多記憶體可被使用:
  • 安裝另一個 new-handler:
  • 卸除 new-handler:
  • 拋出 bad_alloc (或衍生自 bad_alloc 的) exception:
  • 不回返:


如果希望可以藉由被配置物屬於哪個 class,來決定以何種方式處理記憶體配置失敗的情況,可以自行實作出這種行為:
只需令每個 class 提供自己的 set_new_handler 和 operator new 即可:
其中 set_new_handler 讓使用者可以指定 class 專屬的 new-handler。
而 operator new 則確保在配置 class 物件記憶體的過程中,以 class 專屬之 new-handler 取代 global new-handler。


直至 1993 年,C++ 都還要求 operator new 必須在無法配置足夠記憶體時傳回 NULL,但新一代的 operator new 則應該拋出 bad_alloc exception,不過不少 C++ 程式是在 compiler 開始支援新修規格前寫出來的,C++ 標準委員會於是提共另一形式的 operator new,負責供應傳統的「配置失敗便傳回 NULL」行為,這個形式被稱為 "nothrow" 形式:
class Widget { ... };
Widget* pw1 = new Widget;  // 若配置失敗,拋出 bad_alloc
if (pw1 == NULL) ...  // 這個測試一定失敗
Widget* pw2 = new (std::nothrow) Widget;  // 若配置 Widget 失敗,傳回 NULL
if (pw2 == NULL) ...  // 這個測試可能成功

nothrow new 對 exception 的強制保證性並不高,算式 "new (std::nothrow) Widget" 發生兩件事,第一,nothrow 版的 operator new 被喚起,用以配置足夠記憶體給 Widget 物件,如果配置失敗則傳回 NULL。
第二,如果配置成功,Widget constructor 將會被喚起,而在 constructor 中,可能又 new 一些記憶體,而沒人可以再強迫它再次使用 nothrow new。
結論就是,nothrow new 只能保証 operator new 不拋擲 exception,不保證像 "new (std::nothrow) Widget" 這樣的算式絕不導致 exception。


set_new_handler 允許使用者指定一個 function,在記憶體配置無法獲得滿足時被喚起。
nothrow new 是一個頗為侷限的工具,只適用於記憶體配置,後續的 constructor 呼叫還是可能拋出 exception。




50、Understand when it makes sense to replace new and delete.
替換編譯器所提供的 operator new 或 operator delete 的三個最常見理由:
  • 用來檢測運用上的錯誤:如果將「new 所得記憶體」 delete 失敗,會導致 memory leaks,若 delete 多次,則導致不確定從為;此外,若編程錯誤將導致資料 "overruns" 或 "underruns";如果可以自行定義 operator new 及 delete,便可配置超額記憶體,以額外空間放置特定的 byte pattern,而在 delete 時,得以檢查 byte pattern 是否原封不動,以判斷是否有 "overruns" 或 "underruns" 的情況發生。
  • 為了強化效能:compiler 所帶的 operator new 及 delete 主要用於一般目的,處理需求包含大塊記憶體、小塊記憶體、大小混合記憶體,但不對特定項目有最佳表現,所以如果對程式的動態記憶體運用型態有深測瞭解的話,通常可以發現,自定版本的 operator new 及 delete 性能會勝過預設版本。
  • 為了收集使用上的統計數據:在自訂義 operator new 及 delete 之前,理當先收集軟體如何使用其動態記憶體,配置區塊的大小分佈如何?壽命分佈如何?及其它相關資訊等。


static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;

void* operator new(std::size_t size) throw(std::bad_alloc) {
    using namespace std;
    size_t realSize = size + 2 * sizeof(int);  // 增加兩個 int

    void* pMem = malloc(realSize);  // 呼叫 malloc 取得 memory
    if (!pMem) throw bad_alloc();

    *(static_cast(pMem)) = signature;  // 塞入 signature
    *(reinterpret_cast(static_cast(pMem)+realSize-sizeof(int))) = 
            signature;

    return static_cast(pMem) + sizeof(int);
}

這個 operator new 的缺點主是在於疏忽了身為這個特殊函式所應該具備的「堅持 C++ 規矩」的態度,詳見條款 51。

另一個比較微妙的主題是 alignment:
許多 computer architectures 要求特定的型別必須放在特定的記靜勭位址上,如可以會要求指標的位址必須是 four-byte aligned 或 double 的位址必須是 eight-byte aligned,如果沒有奉行,可能導致執行期硬體異常,或效率變差。


總結一下何時可在「全域性的」或「class 專屬的」基本上,合理替換預設的 operator new 或 operator delete :
  • 為了檢測運用錯誤:
  • 為了蒐集動態配置記憶體之使用統計資訊:
  • 為了增加配置和歸還的速度:
  • 為了降低預設記憶體管理器帶來的空間額外開銷:
  • 為了彌補預設配置器中的 suboptimal alignment:
  • 為了相相關物件成簇集中:
  • 為了獲得非傳統的行為:


有許多理由需要寫個自定的 new 及 delete,包括改善效能、對 heap 運用錯誤進行除錯、蒐集 heap 使用資訊…等。



51、Adhere to convention when writing new and delete.
實作一致性 operator new 必得傳回正確的值、記憶體不足時必得呼叫 new-handling function (詳見條款 49)、必須有對付零記憶體需求的準備、還需避免不慎掩蓋正常形式的 new。

operator new 的回返值則較為單純,如果有能力供應使用者申請的記憶體,則傳回一個 pointer 指向那塊記憶體,如果沒有,則遵循條款 49 所描述的規則,並拋出一個 bad_alloc exception。

其實 operator new 實際上不只一次嘗試配置記憶體,且在每次失敗後呼叫 new-handling function,這裡假設 new-handling function 也許能夠做某些動作而將 memory 釋放出來,而只有當 new-handling function 的 pointer 為 null 時,operator new 才會拋出 exception。

按照 C++ 規定,即使客戶要求 0 byte,operator new 也得傳回一個合法指標,下列是個 non-member operator new pseudocode:
void* operator new(std::size_t size) throw(std::bad_alloc) {
    using namespace std;
    if (size == 0) {
        size = 1;
    }

    while (true) {
        if (配置成功)
            return (a pointer,指向配置來的 memory);
        // 配置失敗:找出目前的 new-handling function
        // 就 multi-thread 而言,還需加上 lock 保護
        new_handler globalHandler = set_new_handler(0);
        set_new_handler(globalHandler);

        if (globalHandler) (*globalHandler) ();
        else throw std::bad_alloc();
    }
}

條款 49 談到 operator new 內含一個無窮迴圈,而上述的 pseudocode 明白顯示這個迴圈,而退出此迴圈的方法是:
配憶體被成功配置或 new-handling function 作了一件描述於條款 49 的事情:
讓更多 memory 可用、安裝另一個 new-handler、卸除 new-handler、拋出 bac_alloc excpetion、或是承認失敗直接 return。


寫出訂製型記憶體管理器的一個最常見理由是針對某特定 class 的物件配置行為提供最佳化,卻不是為了該 class 的任何 derived class,也就是說,如果針對 class X 而設計的 operator new,其行為很典型地只為大小剛好為 sizeof(X) 的物件而設計,一旦被繼承,有可能 base class 的 operator new 被呼叫用以配置 derived class 物件:
class Base {
  public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
};
class Derived : public Base {  // 若 Derived 未宣告 operator new
    ...
};
Derived* p = new Derived;  // 這裡呼叫的是 Base::operator new

如果 Base class 專屬的 operator new 並非被設計用來對付上述情況,處理此情勢的最佳作法是將「記憶體申請量錯誤」的呼叫行為改採標準 operator new:
void* Base::operator new(std::size_t size) throw(std::bad_alloc) {
    if (size != sizeof(Base))  // 如果 size 錯誤,令標準的
        return ::operator new(size);  // operator new 處理
    ...
}


而 operator delete 的情況更簡單,需要記住的唯一事情就是 C++ 保証「刪除 null pointer 永遠安全」,所以必須兌現這項保証:
void operator delete(void *rawMemory) throw() {
    // 如果是 null pointer,則 do nothing
    if (rawMemory == 0) return;

    // 現在,歸還 rawMemory 所指的 memory
}

而 operator delete 的 member 版本,也只需要多加一個動作檢查刪除數量:
class Base {
  public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    static void operator delete(void* rawMemory, std::size_t size) throw();
};

void Base::operator delete(void* rawMemory, std::size_t size) throw() {
    if (rawMemory ==0) return;
    if (size != sizeof(Base)) {
        ::operator delete(rawMemory);
        return;
    }
    // 歸還 rawMemory 所指的 memory
    return;
}


如果即將被刪除的物件衍生自某個 base class,而此 base class 欠缺 virtual destructor,則 C++ 傳給 operator delete 的 size_t 數值可能會不正確,所以必須「讓 base class 擁有 virtual destructor」,此外條款 7 也有另外的理由。


operator new 應該內含一個無窮迴圈,並在其中嘗試 allocate memory,如果無法滿足需求,就該呼叫 new-handler;operator new 也應該有能力處理 0 byte 申請;若是 class 專屬版本則還應該處理「比正確 size 更大的申請」。
operator delete 應該在收到 null pointer 時,不作任何事;若是 class 專屬版本則還應該處理「比正確 size 更大的申請」。




52、Write placement delete if you write placement new.
回憶條款 16, 17 中,一個 new 算式:
Widget* pw = new Widget;

共有兩個 function 被呼叫:
一個是用以配置記憶體的 operator new,另一個是 Widget 的 default constructor。

假設第一個 function 呼叫成功,第二個 function 卻發出 exception,如此,步驟一的記憶體配置所得必須取消並恢復舊觀,否則會造成 memory leak,而取消步驟一並恢復舊觀的責任就在 C++ 執行期系統身上。

在執行期系統呼叫步驟一所呼叫的 operator new 的相應 operator delete 版本,前提是必須知道哪一個 operator delete 該被呼叫,如果是擁有正常 signature 的 new 和 delete,則不是問題,因為正常的 operator new 對應於正常的 operator delete:
void* operator new(std::size_t) throw(std::bad_alloc);
// global 作用域中的正常簽名式
void operator delete(void* rawMemory) throw();
// class 作用域中的正常簽名式
void operator delete(void* rawMemory, std::size_t size) throw();

然而當開始宣告非正常形式的 operator new,也就是帶有附加參數的 operator new,「究竟哪個 delete 伴隨這個 new」的問題便浮便了。

假設寫了一個 class 專屬的 operator new,要求一個 ostream 用來誌記相關配置資訊,同時又寫了一個正常形式的 class 專屬 operator delete:
class Widget {
  public:
    ...
    static void* operator new(std::size_t size,
            std::ostream& logStream) throw(std::bad_alloc);
    // 非正常形式的 new
    static void operator delete(void* pMemory std::size_t size)
        throw();  // 正常形式的 delete
    ...
};

這個設計有問題,但在探討原因之後,先扼要討論若干術語。

如果 operator new 接受的參數除了一定會有的 size_t 之外,還有其它,這便是所謂的 placement new,因此上述的 operator new 是個 placement 版本,而眾多 placement new 版本中特別有用的一個是「接受一個指標指向該被建構之處」:
void* operator new(std::size_t, void* pMemory) throw();
// placement new

當人們談到 placement new,大多時候所談的是此一特定版本,也就是「唯一額外引數是個 void*」,少數時候才是指接受任何額外引數之 operator new。
一般性術語 "placement new" 意味帶任意參數的 new ,而另一個術語 "placement delete" 直接衍生自它。

回到 Widget class 的宣告式:
Widget* pw = new (std::cerr) Widget;
// 呼叫 operator new 並傳遞 cerr 為其 ostream 引數,
// 會在 Widget constructor 拋出 exception 時造成 memory leak

如果記憶體配置成功,而 Widget constructor 拋出 exception,執行期系統有責任取消 operator new 的配置並恢復舊觀,然而執行期系統無法得知真正被喚起的那個 operator new 如何運作,因此無法取消配置並恢復舊觀,取而代之的是,執行期系統尋找「參數個數和型別都與 operator new 相同」的某個 operator delete。
如果找到,那就是它的呼叫對象:
void operator delete(void*, std::ostream&) throw();

如果找不到,則什麼都不做,這就會造成 memory leak。


所以規則很簡單:
如果一個帶額外參數的 operator new 沒有「帶相同額外參數」的對應版 operator delete,那麼當 new 的記憶體配置動作需要取消並恢復舊觀時,就沒有任何 operator delete 會被喚起。
因此,為了消弭稍早程式碼中的記憶體洩漏,Widget 有必要宣告一個 placement delete,對應於 placement new:
class Widget {
  public:
    ...
    static void* operator new(std::size_t size,
            std::ostream& logStream) throw(std::bad_alloc);
    // 非正常形式的 new
    static void operator delete(void* pMemory std::size_t size)
        throw();  // 正常形式的 delete
    static void operator delete(void* pMemory std::size_t size,
            std::ostream& logStream) throw();
    // 非正常形式的 delete
    ...
};


附帶一提,由於 member function 的名稱會掩蓋其外圍作用域中的相同名稱,詳見條款 33,所以必須小心避免讓 class 專屬的 operator new 掩蓋使用者期望的其它 operator new,如果有一個 base class,其中宣告了唯一一個 placement operator new,則使用者會發現他們無法使用正常形式的 new:
class Base {
  public:
    ...
    static void* operator new(std::size_t size, std::ostream& logStream)
            throw(std::bad_alloc);  // 會遮掩正常的 global 型式
    ...
};

Base* pb = new Base;  // 錯誤,正式型式的 operator new 被遮掩
Base* pb = new (std::cerr) Base;  // 正確
同樣道理,derived class 中的 operator new 也會遮蓋 global 版本和繼承而得的 operator new:
class Derived : public Base {
  public:
    ...
    static void* operator new(std::size_t size, std::ostream& logStream)
            throw(std::bad_alloc);  // 重新宣告正常型式的 new
    ...
};

Derived* pd = new (std::clog) Derived;
// 錯誤,Base 的 placement new 被遮掩了
Derived* pd = new Derived;  // 正確

條款 33中有詳細討論這種名稱遮掩問題,預設情況下,C++ 在 global 作用域內提供以下形式的 operator new:
void* operator new(std::size_t) throw(std::bad_alloc);
// normal new
void* operator new(std::size_t, void*) throw();  // placement new
void* operator new(std::size_t, const std::nothrow_t&) throw();
// nothrow new

在 class 內宣告任保 operator new,都會遮掩上述這些標準形式,除非是刻意要阻止 class 的使用者使用這些形式,否則需確保它們在所生成的任保訂製型 operator new 之外都還可以被使用,而對每個可用的 operator new 也需確定提供對應的 operator delete,如果希望這些 function 有著平常的行為,只要令 class 專屬版本呼叫 global 版本即可。
完成上述所言的一個簡單作法是,建立一個 base class,內含所有正常形式的 new 和 delete:
class StandardNewDeleteForms {
  public:
    // normal new / delete
    static void* operator new(std::size_t size) throw(std::bad_alloc) {
        return ::operator new(size);
    }
    static void* operator delete(void* pMemory) throw() {
        ::operator delete(pMemory);
    }
    // placement new / delete
    static void* operator new(std::size_t size, void* ptr)
            throw() {
        return ::operator new(size, ptr);
    }
    static void* operator delete(void* pMemory, void* ptr)
            throw() {
        ::operator delete(pMemory, ptr);
    }
    // nothrow new / delete
    static void* operator new(std::size_t size,
            const std::nothrow_t& nt) throw() {
        return ::operator new(size, nt);
    }
    static void* operator delete(void* pMemory,
            const std::nothrow_t&) throw() {
        ::operator delete(pMemory);
    }
};

而凡是想以自定型式擴充標準型式的使用者,可利用 inheritance 及 using 宣告式取得標準型式:
class Widget : public StandardNewDeleteForms {
  public:
    // 讓這些型式可見
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForms::operator delete;
    // 添加一個自定的 placement new
    static void* operator new(std::size_t size,
            std::ostream& logStream) throw(std::bad_alloc);
    // 添加一個自定的 placement delete
    static void operator delete(void* pMemory,
            std::ostream& logStream) throw();
   ...
};


當寫一個 placement operator new,也須確定寫出對應的 placement operator delete,以免發生 memory leak。
如果宣告了 placement new 及 placement delete,需多加一些手法,避免遮掩了它們的正常版本。
arrow
arrow
    全站熱搜

    silverfoxkkk 發表在 痞客邦 留言(0) 人氣()