目录
基础
不知各位是否遇到过一些代码需要静态注册什么的, 最简单粗暴的方式应该很多人都用过:
class MyInit {
public:
MyInit(void) {
// do init step
}
~MyInit(void) {
// do cleanup step
}
};
// in cpp files only
static MyInit _init;
原理也很简单粗暴, 利用全局静态对象, 在构造时调用所需的初始化代码
header only
一般来说上面的解决方案就足够大多数场景使用了, 不过还有一种搞不定的就是 header only 的库,
不希望还得单独搞个 cpp 来实现, 一是麻烦 (尤其量多了之后), 二是容易忘
直接将上述内容放头文件里其实也能达到类似效果, 但是有明显的问题:
- 每个编译单元引入这个头文件, 就会生成一个静态对象实例
- 由于有多个静态对象实例, 会造成初始化方法被调用多次
(当然这个有方法可以绕过, 比如初始化时只注册函数指针, 然后在某个统一入口来调用函数指针进行初始化) - 当引入这个头文件的编译单元多了之后, 会造成这些无意义的静态对象实例的个数爆炸性的增长,
造成内存或性能问题
当然, C艹是个神奇的语言, 还是有方法可以解决这问题, 答案自然是 C艹 神奇的模板:
template<typename D>
class Wrapper {
public:
class ExecInit {
public:
ExecInit(void) {
// do init step
}
~ExecInit(void) {
// do cleanup step
}
};
static ExecInit register_object;
template<ExecInit &> class RefIt {};
static RefIt<register_object> ref_object;
};
template<typename D>
typename Wrapper<D>::ExecInit Wrapper<D>::register_object;
class RefItDummy : Wrapper<RefItDummy> {};
- 核心原理和之前的其实没什么差别, 主要变化之一是, 用模板去包装静态对象,
以便能够在头文件中申明并实例化
(非模板的静态对象, 直接放头文件是会符号重定义的,
模板就可以保证每个特化只会有一份) - 多了个 RefIt 和 RefItDummy 主要是为了防止模板没有被显式引用时,
被编译器优化掉
class scope
有了上述 header only 的方案, 基本上可以解决绝大多数需求了, 但是奇葩需求总还是会有的,
比如, 如何在 class scope 里面搞?
其实还是可以用同一套轮子再搞个复杂点的版本, 就能实现了:
// 首先, 全局还是得要有个 wrapper
// 但是这回我们再多加点料
template<typename ExecWrapper>
class Wrapper {
public:
class ExecInit {
public:
ExecInit(void) {
ExecWrapper::myInit();
}
~ExecInit(void) {
ExecWrapper::myCleanup();
}
};
static ExecInit register_object;
template<ExecInit &> class RefIt {};
static RefIt<register_object> ref_object;
};
template<typename ExecWrapper>
typename Wrapper<ExecWrapper>::ExecInit Wrapper<ExecWrapper>::register_object;
class RefItDummy : Wrapper<RefItDummy> {
public:
static void myInit(void) {}
static void myCleanup(void) {}
};
// 然后, 用同样的套路来一遍模板特化, 以确保 register_object 被实例化
class MyObject {
public:
class MyInit {
public:
static void myInit(void) {
// do init step
}
static void myCleanup(void) {
// do cleanup step
}
};
static Wrapper<MyInit> &refIt(void) {
static Wrapper<MyInit> d;
return d;
}
};
// 注意上述代码不需要改动到自己原先的 MyObject, 而且可以直接在里面添加任意多个的注册入口
// 也不需要引用到 MyObject 时才会初始化, 只要引入了头文件就自动会调用
- 利用的还是同一个套路, 只要模板的特化版本被引用, 特化版本内部的静态对象就会被初始化,
并最终调用我们的初始化代码 - 当然这里面也有不少 tricks, 比如模板为什么要定义在全局范围内,
比如 MyInit 为什么是作为模板参数来传递
注意事项
虽然上述模板的方式看似很午很完美, 但是至少目前为止已发现了个大坑: 请尤其注意避免过度依赖这种方式,
否则在 Windows 平台可能会出现 too many sections
, file too big
, 或者类似问题
至于原因:
- 模板方式实现不会出现符号重定义, 是因为模板实例可以在链接过程中被优化掉
(编译器能够明确知道这些模板实例是相同的, 更多的东西, 有兴趣可以搜一下ODR (One Defination Rule)
) - 然而在编译过程中, 每个 cpp 其实是会实例化所有引用到的模板,
所以, 如果一个 cpp 引入的上述显式实例化的模板多了,
就会造成这个编译单元内的对象过多, 最终造成too many sections
当然:
- 这玩意儿只在 Windows 平台会出现, 似乎是因为旧的 PE 格式只有两字节存储对象个数,
所以最大个数是喜闻乐见的 32768 左右的大小,
对于重度模板用户来说, 还是比较容易超过的
(boost 用户也请注意类似问题, 股沟已经搜出不少先例了, 而且没有什么好的解决方案) - 如果用的是 Visual Studio, 还可以用
/bigobj
编译选项绕过一下,
但是 MinGW 之流就完全没辙了
个人建议:
- 谨慎使用该方式, 尤其是你有计划跨平台的时候,
因为头文件弄这些, 是很可能污染其他代码的, 到时候要收手可不是容易的事
转载请注明来自: http://zsaber.com/blog/p/152
既然都来了, 有啥想法顺便留个言呗? (无奈小广告太多, 需审核, 见谅)