C++ 静态注册那些事儿

By | 2017-01-08

基础

不知各位是否遇到过一些代码需要静态注册什么的, 最简单粗暴的方式应该很多人都用过:

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

既然都来了, 有啥想法顺便留个言呗? (无奈小广告太多, 需审核, 见谅)

Category: C++

发表评论

您的电子邮箱地址不会被公开。