更多 C++ 習語/巧妙計數器
確保非區域性靜態物件在首次使用之前初始化,並且僅在最後使用該物件後銷燬。
當靜態物件使用其他靜態物件時,初始化問題變得更加複雜。如果靜態物件具有非平凡的初始化,則必須在使用之前對其進行初始化。跨編譯單元的靜態物件的初始化順序沒有明確定義。多個靜態物件(分佈在多個編譯單元中)可能正在使用單個靜態物件。因此,必須在使用之前對其進行初始化。一個例子是std::cout,它通常被其他幾個靜態物件使用。
“巧妙計數器”或“Schwarz 計數器”習語是將引用計數習語應用於靜態物件初始化的一個例子。
// Stream.h
#ifndef STREAM_H
#define STREAM_H
struct Stream {
Stream ();
~Stream ();
};
extern Stream& stream; // global stream object
static struct StreamInitializer {
StreamInitializer ();
~StreamInitializer ();
} streamInitializer; // static initializer for every translation unit
#endif // STREAM_H
// Stream.cpp
#include "Stream.h"
#include <new> // placement new
#include <type_traits> // aligned_storage
static int nifty_counter; // zero initialized at load time
static typename std::aligned_storage<sizeof (Stream), alignof (Stream)>::type
stream_buf; // memory for the stream object
Stream& stream = reinterpret_cast<Stream&> (stream_buf);
Stream::Stream ()
{
// initialize things
}
Stream::~Stream ()
{
// clean-up
}
StreamInitializer::StreamInitializer ()
{
if (nifty_counter++ == 0) new (&stream) Stream (); // placement new
}
StreamInitializer::~StreamInitializer ()
{
if (--nifty_counter == 0) stream.~Stream ();
}
Stream 類的標頭檔案必須在對Stream 物件呼叫任何成員函式之前包含。在每個編譯單元中都包含了StreamInitializer 類的例項。對Stream 物件的任何使用都在包含標頭檔案之後,這確保了在使用Stream 物件之前呼叫了初始化器物件的建構函式。
Stream 類的標頭檔案聲明瞭對Stream 物件的引用。此外,該引用是extern,這意味著它在一個翻譯單元中定義,並且對它的訪問是由連結器而不是編譯器解析的。Stream 類的實現檔案最終定義了Stream 物件,但使用了一種不尋常的方式:它首先定義了一個靜態(即特定於翻譯單元)緩衝區。該緩衝區既正確對齊又足夠大以儲存型別為Stream 的物件。然後,將標頭檔案中定義的Stream 物件的引用設定為指向該緩衝區。
這種緩衝區變通方法使我們可以精細地控制何時呼叫Stream 物件的建構函式和解構函式。在上面的示例中,建構函式是在第一個StreamInitializer 物件的建構函式中呼叫的,使用 placement new 將其放置在緩衝區中。Stream 物件的解構函式是在最後一個StreamInitializer 物件被銷燬時呼叫的。
這種變通方法是必要的,因為在 Stream.cpp 中定義一個Stream 變數(無論是靜態的還是非靜態的) - 都會在StreamInitializer 之後定義它,而StreamInitializer 是透過包含標頭檔案來定義的。然後,StreamInitializer 的建構函式將在Stream 的建構函式之前執行,更糟糕的是,初始化器的解構函式將在Stream 物件的解構函式之後執行。上面的緩衝區解決方案避免了這種情況。
標準 C++ <iostream> 庫std::cout、std::cin、std::cerr、std::clog。