By Matthew Wilson
树人 译
11.4静态成员
在讲述静态对象的一章中,不涵盖静态成员是很无礼的,所以我们现在来看看静态成员。这一节中的一些问题是新的;另一些则是早先章节中提出的问题的一个缩影。
11.4.1拦截链接器问题
有些时候,你会为程序库的类或函数编码,这些类或函数不会在进程,当前用户或系统会话的生存期中更改,甚至是在系统安装的生存期中。如此的话,每次你想存取常量信息的时候都去调用这些组件是非常令人不快的,特别是在这个信息自身可能非常昂贵的时候。
一个好的例子就是Win32高性能计数器API,它有两个函数构成:
BOOL QueryPerformanceCounter(LARGE_INTEGER *);
BOOL QueryPerformanceFrequency(LARGE_INTEGER *);
每次都要使用一个64-bit的整数。第一个返回系统的硬件高性能计数器的当前值。第二个返回计数器的频率,在系统会话的生存期中它不能更改,而且在实践中,它是一个代表给定的处理机的常数。这个频率用于把由QueryPerformanceCounter()返回的机器指定的初相(信号出现时间)转化为实际的时间间隔。
自然,在使用这样一个裸露的(无保护的)API时候,我的C++直觉会立竿见影,所以在我第二次必须使用到它的时候我会编写一个包装类(wrapper)[12]。high_performance_counter类维护了代表了一个时间间隔的两个整数(通过调用start()和stop()方法来获得),而且还提供了把间隔解释成秒,毫秒或微秒的方法。问题是这些API函数都有非常大的调用开销[Wils2003a],所以要谨慎地通过缓存值来避免对QueryPerformanceFrequency()的不必要的重复调用。这是典型的静态成员的情形。但是,由于STLSoft必须是100%的头文件的指导原则[13],这提出了一个挑战。一种方法就是在一个静态方法中使用一个局部于函数的静态实例,像下边这样:
[12]Robert L.Glass构造了一种避免过早泛化的引人注目的情况,通过延迟代码从具体到可重用(泛化)的转换,直到第二次需要它时才进行转换,我倾向于赞同他。Robert L. Glass , Facts and Fallacies of Software Engineering, Addison-Wesley.
[13]我必须承认我有一种个人的,可能是独断的偏见,against implementation files that are needed only for static members.那就是不认为只有静态成员才需要实现文件。心平气和的时候我就知道这样做是否合理。但这似乎不会动摇我对无拘无束的“原子弹”#include的钟爱。无论合理还是不合理,它都是用来提供大量(我希望是)与静态对象主题相关的材料的。
class high_performance_counter;
{
. . .
private:
static LARGE_INTEGER const &frequency()
{
static LARGE_INTEGER s_frequency;
return s_frequency;
}
. . .
但以上只是代码的一半;此处还没有初始化。答案就是通过调用另一个用来检索频率值的静态方法来初始化s_frequency,像Listing11.11所示:
class high_performance_counter;
{
. . .
static LARGE_INTEGER const query_frequency()
{
LARGE_INTEGER freq;
if(!QueryPerformanceFrequency(&freq))
{
freq = std::numeric_traits<sint64_t>::max();
}
return freq;
}
static LARGE_INTEGER const &frequency()
{
static LARGE_INTEGER s_frequency = query_frequency();
. . .
注意,如果系统不支持一个硬件性能计数器,而且QueryPerformanceCounter()返回FALSE,那么值被设置为最大值,因此其后的时间间隔除法不会导致一个除零错误,但会直接返回0。我选择这种方法适因为这些性能测量类是为代码profiling而提供的,而不是作为应用程序功能的一部分;你可能期望抛出一个异常。
但这里还有些不完全,因为我们是在使用一个局部于函数的静态对象,而且往往会遭受竞争条件。在这种情况下,可能发生的最坏情况是在罕见的情形下query_frequency()被多次调用。因为总是返回相同的值,同时我们也需要性能计数器自身尽可能的廉价,所以我选择罕见但良性的多重初始化。
11.4.2自适应代码
此前我们结束了静态成员的话题,现在我非常愿意向你展示一种最终的鬼魅的技巧[4],它允许你编写一些可以使其自身行为适应其所在的不同情形的类。
[14]在此我并不是在鼓励自大和无故叛逆的态度,这些离有自豪感的软件工程太远了。但我也不相信向人们隐瞒复杂而又危险的事情会有多大帮助;看看Java!我说这些是为了告诉你在学习的过程中,观察错误的方法和观察正确的方法具有同样的价值。
上一节所描述的那个性能计数器类有些问题,因为如果类的用户不能得到硬件计数器的话,会使无效计时都读成0。事实上,Win32支持其它的计时函数,其中一个就是GetTickCounter(),在所有平台下都能使用,但分辨率更低[Wils2003a]。因此,另一个类performance_counter试图在可能的情况下提供一个高分辨率的测量,在不可能的情况下提供一个低分辨率的。
为了做到这些,避免对QueryPerformanceCounter()的直接调用以支持调用一个静态方法measure(),如下所示的定义Listing11.12:
Listing 11.12.
class performance_counter
{
. . .
private:
typedef void (*measure_fn_type)(epoch_type&);
static void performance_counter::qpc(epoch_type &epoch)
{
QueryPerformanceCounter(&epoch);
}
static void performance_counter::gtc(epoch_type &epoch)
{
epoch = GetTickCount();
}
static measure_fn_type get_measure_fn()
{
measure_fn_type fn;
epoch_type freq;
fn = QueryPerformanceFrequency(&freq) ? qpc : gtc;
return fn;
}
static void measure(epoch_type &epoch)
{
static measure_fn_type fn = get_measure_fn();
fn(epoch);
}
// Operations
public:
inline void performance_counter::start()
{
measure(m_start);
}
inline void performance_counter::stop();
. . .
函数frequency()和query_frequency()和以前是一样的,除了在QueryPerformanceCounter()失效时把频率设置为1000以外,这是为了反映GetTickCounter返回毫秒单位并给出一个有效的时间间隔除法的事实。
11.5静态终结语
浓缩在一起的话,静态对象的问题就是次序,一致性和同步。不难想象对于这三个问题语言本身很容易犯错误。次序问题只有在你使用几个相互依赖的全局对象时才会出现:但由于这种对象不至于有太多(仅仅是奇异的cout和cin),所以它们使用的前期并不会形成什么问题。一致性问题只有当你的一个可执行程序拥有多个编译单元的时候才真正出现;动态链接,并不是一个全新的技术,但也不至于老到成为C++的初始设计过程中的基本重要。同步问题只有在多线程的背景下才会出现;如同动态库一样,多线程是在C++的初始设计完成后渐渐流行起来的东西。
并不是说只要语言的原始设计者能深谋远虑或者有一个时间机器就可以避免所有的这些问题。之后的语言仍不能令人信服地定位这些问题:有时是通过禁止那些要求所有对象都基于堆的有问题的构建;有时干脆就不定位它们。新语言对什么有效,迄今为止能够避免线程问题?
如此,我会提出我那平凡的建议来供参考。在问题出现的时候,唯一正确的解决方案就是重视它,使用技术来消除它,至少也要修正它。其中大多数技术概念上都是简单明了的,尽管它们的实现可能会很冗长乏味(例如:基于API的单件)。
在离开本章之前,你不认为对于依赖于static关键字本身的static的不同用法的多数问题的解答有几分的敏锐吗?当然,接下来还有那古老的自旋互斥量…