更多 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
。