再探C++自动注册

最近的c++项目里面需要使用配置文件配置的值去决定运行时具体实例化的类。如果是java或者kotlin直接使用反射去创建就好,但是c++里没有类似的东西所以只能通过一些取巧的方式实现。

早年间有研究过GTest的测试用例注册机制,其原理是利用类的静态成员变量在初始化的时候在它构造函数里面执行注册代码。

包括我们部门现在更常用的Catch2也是类似的,只不过它用的是全局的Catch::AutoReg const常量而不是类静态成员变量:

#define TEST_CASE( ... ) INTERNAL_CATCH_TESTCASE( __VA_ARGS__ )

#define INTERNAL_CATCH_TESTCASE2( TestName, ... ) \
        static void TestName(); \
        CATCH_INTERNAL_START_WARNINGS_SUPPRESSION \
        CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS \
        CATCH_INTERNAL_SUPPRESS_UNUSED_VARIABLE_WARNINGS \
        namespace{ const Catch::AutoReg INTERNAL_CATCH_UNIQUE_NAME( autoRegistrar )( Catch::makeTestInvoker( &TestName ), CATCH_INTERNAL_LINEINFO, Catch::StringRef(), Catch::NameAndTags{ __VA_ARGS__ } ); } /* NOLINT */ \
        CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION \
        static void TestName()
    #define INTERNAL_CATCH_TESTCASE( ... ) \
        INTERNAL_CATCH_TESTCASE2( INTERNAL_CATCH_UNIQUE_NAME( CATCH2_INTERNAL_TEST_ ), __VA_ARGS__ )

struct AutoReg : Detail::NonCopyable {
    AutoReg( Detail::unique_ptr<ITestInvoker> invoker, SourceLineInfo const& lineInfo, StringRef classOrMethod, NameAndTags const& nameAndTags ) noexcept;
};

AutoReg::AutoReg( Detail::unique_ptr<ITestInvoker> invoker, SourceLineInfo const& lineInfo, StringRef classOrMethod, NameAndTags const& nameAndTags ) noexcept {
    CATCH_TRY {
        getMutableRegistryHub()
                .registerTest(
                    makeTestCaseInfo(
                        extractClassName( classOrMethod ),
                        nameAndTags,
                        lineInfo),
                    CATCH_MOVE(invoker)
                );
    } CATCH_CATCH_ALL {
        // Do not throw when constructing global objects, instead register the exception to be processed later
        getMutableRegistryHub().registerStartupException();
    }
}

所以我们可以类似的去定义一个AutoRegister类在构造函数里面将类的工厂函数注册给Factory:

// factory.h
typedef std::shared_ptr<IComponent> (*CreatorFunc)();

class Factory {
public:
    static Factory& Instance();
    
    Factory();

    void RegisterCreatorFunc(const std::string& name, CreatorFunc func);

    std::shared_ptr<IComponent> Create(const std::string& name);

private:
    std::map<std::string, CreatorFunc> creators_;
};

class AutoRegister {
public:
    AutoRegister(const std::string& name, CreatorFunc func);
};

// factory.cpp
void Factory::RegisterCreatorFunc(const std::string& name, CreatorFunc func) {
    creators_[name] = func;
}

std::shared_ptr<IComponent> Factory::Create(const std::string& name) {
    auto it = creators_.find(name);
    if (it == creators_.end()) {
        std::cout<<"Unknown : " << name <<std::endl;
        return nullptr;
    }
    auto instance = it->second();
    return instance;
}

AutoRegister::AutoRegister(const std::string& name, CreatorFunc func) {
    Factory::Instance().RegisterCreatorFunc(name, func);
}

然后定义一个宏去协助声明全局变量:

#define AUTO_REGISTER(NAME) \
static std::shared_ptr<IComponent> __##NAME##_ComponentCreatorFunc(){return std::make_shared<NAME>();} \
const AutoRegister __##NAME##_ComponentRegister(#NAME, __##NAME##_ComponentCreatorFunc);

typedef std::shared_ptr<IComponent> (*CreatorFunc)();

最后只需要在定义IComponent子类的时候使用AUTO_REGISTER注明一下需要自动注册即可:

// component_a.h
class ComponentA : public IComponent {
public:
    void Init() override;
};

AUTO_REGISTER(ComponentA)

// component_a.cpp
void ComponentA::Init() {
    std::cout << "ComponentA::Init()" <<std::endl;
}

原理和实现都比较简单,无法是利用全局变量在main函数执行前初始化的机制,在全局变量的构造函数里面做事情。但实际使用的时候会有不少的坑。

全局变量的初始化顺序不确定

正如我之前的博客讨论的全局变量的初始化顺序时是不能确定的。

static const std::string kGlobalStr = "GlobalStr";
#define GLOBAL_STR "GLOBAL_STR"

Factory& Factory::Instance() {
    static Factory instance;
    return instance;
}

Factory::Factory() {
    std::cout << "kGlobalStr : " << kGlobalStr << std::endl;
    std::cout << "GLOBAL_STR : " << GLOBAL_STR << std::endl;
}

单例的instance我们可以通过局部静态变量的方式保证在第一次调用Factory::Instance的时候会初始化,但是如果在Factory的构造函数里面使用了其他的全局变量可能就会拿到还未初始化的变量:

kGlobalStr :
GLOBAL_STR : GLOBAL_STR

我们可以用宏或者再写个函数把全局变量变成局部静态变量包在函数里面去解决。

静态库依赖的情况下不会自动注册

还有另外一个问题是如果我们的组件是放在静态库里面去依赖的话,没有include的全局变量不会初始化:

add_library(${PROJECT_NAME}-Lib STATIC
    ../factory.cpp
    ../component_a.cpp
    ../component_b.cpp
)

add_executable(${PROJECT_NAME}
    ../main.cpp
)

target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}-Lib)
#include "factory.h"
#include "component_b.h"

int main(int argc,char **argv) {
    // 没有#include "component_a.h", 静态库依赖的时候ComponentA没有自动注册
    auto a = Factory::Instance().Create("ComponentA");
    if(a != nullptr) {
        a->Init(); 
    }

    // 有#include "component_a.h", 静态库依赖的时候ComponentB有自动注册
    auto b = Factory::Instance().Create("ComponentB");
    if(b != nullptr) {
        b->Init(); 
    }
    return 0;
}
kGlobalStr :
GLOBAL_STR : GLOBAL_STR
Unknown : ComponentA
ComponentB::Init() - 0x600001848288

在我们的事件项目中把Catch2编写的测试用例改成静态库依赖同样也会出现Catch2找不到测试用例的问题,网上没有找到什么太清晰的解答,但是chatgpt给了下面的回答:

关于全局静态变量的初始化行为,C和C++标准并没有明确规定。具体的行为可能会因编译器和链接器的实现而有所不同。

然而,根据常见的编译器和链接器的实践,可以得出以下一般性规则:

1. 静态库:
   - 在静态库中,未被引用的全局静态变量通常不会被初始化。
   - 这是因为链接器会进行优化,只将被引用的目标文件和符号与主程序进行链接,未被引用的目标文件和符号会被优化掉。

2. 源码依赖:
   - 在源码依赖的情况下,全局静态变量通常会被初始化,无论是否被引用。
   - 这是因为编译器会对源码进行逐行解析和编译,将所有的全局静态变量初始化代码转换为可执行的指令。

虽然没有明确的权威文件规定这些行为,但这些规则是根据广泛的实践和经验总结得出的。

如果您对特定编译器和链接器的行为有疑问,建议查阅它们的官方文档或相关规范,以了解更具体的行为和规定。不同的编译器和链接器可能会有不同的实现和行为。

在mac上使用nm名搜索符合表,也的确可以看到没有__ComponentA_ComponentRegister这个符号:

nm ./Demo| grep -e ComponentA_ComponentRegister -e ComponentB_ComponentRegister
0000000100010040 b __ZL30__ComponentB_ComponentRegister
0000000100010000 b __ZL30__ComponentB_ComponentRegister

enable_shared_from_this的问题

这个问题和自动注册没有直接关系,但是设计不好可能会遇到所以就放一起了。假如我们的工厂函数返回值不是shared_ptr<IComponent>而是IComponent*,那么子类在使用enable_shared_from_this的时候就会出现问题:

// factory.h
#define AUTO_REGISTER(NAME) \
static IComponent* __##NAME##_ComponentCreatorFunc(){return new NAME();} \
const AutoRegister __##NAME##_ComponentRegister(#NAME, __##NAME##_ComponentCreatorFunc);

typedef IComponent* (*CreatorFunc)();

// factory.cpp
std::shared_ptr<IComponent> Factory::Create(const std::string& name) {
    auto it = creators_.find(name);
    if (it == creators_.end()) {
        std::cout<<"Unknown : " << name <<std::endl;
        return nullptr;
    }
    auto instance = it->second();
    return std::shared_ptr<IComponent>(instance);
}

// component_b.h
class ComponentB : public IComponent, public std::enable_shared_from_this<ComponentB> {
public:
    void Init() override;
};

AUTO_REGISTER(ComponentB)

// component_b.cpp
void ComponentB::Init() {
    std::cout << "ComponentB::Init() - " << this->shared_from_this() <<std::endl; // 抛出bad_weak_ptr异常
}

调用到ComponentB::Init之后就会抛出异常:

libc++abi: terminating with uncaught exception of type std::__1::bad_weak_ptr: bad_weak_ptr

从源码来看用裸指针创建shared_ptr的时候会调用__enable_weak_this:

template<class _Yp>
    explicit shared_ptr(_Yp* __p,
                        typename enable_if<__compatible_with<_Yp, element_type>::value, __nat>::type = __nat());

template<class _Tp>
template<class _Yp>
shared_ptr<_Tp>::shared_ptr(_Yp* __p,
                            typename enable_if<__compatible_with<_Yp, element_type>::value, __nat>::type)
    : __ptr_(__p)
{
    unique_ptr<_Yp> __hold(__p);
    typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT;
    typedef __shared_ptr_pointer<_Yp*, __shared_ptr_default_delete<_Tp, _Yp>, _AllocT > _CntrlBlk;
    __cntrl_ = new _CntrlBlk(__p, __shared_ptr_default_delete<_Tp, _Yp>(), _AllocT());
    __hold.release();
    __enable_weak_this(__p, __p);
}

只有裸指针的类型是enable_shared_from_this的时候才会去初始化它的__weak_this_:

template <class _Yp, class _OrigPtr>
    _LIBCPP_INLINE_VISIBILITY
    typename enable_if<is_convertible<_OrigPtr*,
                                      const enable_shared_from_this<_Yp>*
    >::value,
        void>::type
    __enable_weak_this(const enable_shared_from_this<_Yp>* __e,
                       _OrigPtr* __ptr) _NOEXCEPT
    {
        typedef typename remove_cv<_Yp>::type _RawYp;
        if (__e && __e->__weak_this_.expired())
        {
            __e->__weak_this_ = shared_ptr<_RawYp>(*this,
                const_cast<_RawYp*>(static_cast<const _Yp*>(__ptr)));
        }
    }

_LIBCPP_INLINE_VISIBILITY void __enable_weak_this(...) _NOEXCEPT {}

也就是说如果子类继承了enable_shared_from_this,但是转换成非enable_shared_from_this的基类指针去创建shared_ptr由于已经不是enable_shared_from_this,所以根据入参会匹配到_LIBCPP_INLINE_VISIBILITY void __enable_weak_this(...) _NOEXCEPT {}这个空实现。未对__weak_this_赋值。

所以在子类里面调用this->shared\_from\_this()就会因为__r.__cntrl_为0抛出bad_weak_ptr异常:

// enable_shared_from_this:
hared_ptr<_Tp const> shared_from_this() const
        {return shared_ptr<const _Tp>(__weak_this_);}


// shared_ptr:
template<class _Tp>
template<class _Yp>
shared_ptr<_Tp>::shared_ptr(const weak_ptr<_Yp>& __r,
                            typename enable_if<is_convertible<_Yp*, element_type*>::value, __nat>::type)
    : __ptr_(__r.__ptr_),
      __cntrl_(__r.__cntrl_ ? __r.__cntrl_->lock() : __r.__cntrl_)
{
    if (__cntrl_ == 0)
        __throw_bad_weak_ptr();
}

从这个案例我们得到的教训就是使用智能指针就都是用智能指针,不要和裸指针混着用,哪天就踩坑了。

总结

技术选型的时候要充分了解技术的原理和可能出现的问题,然后根据具体业务场景去考虑是否合适。

例如这里的自动注册技术用于单元测试的测试用例注册是合适的,在单元测试的场景里不会遇到全局变量初始化问题,正常情况下也不会把测试用例写在单独的静态库里而是直接编译测试的可执行程序。

而对我们这个项目的场景就不合适了,因为两者我们都实际有可能遇到问题。当然可以在文档里面写明最佳实践方式,但是一旦不小心后人就会踩坑。

所以我会选择稳妥点,在一处地方集中手动编写注册代码。虽然这样每加一个组件除了.h和.cpp还需要再在另外的地方添加注册代码,但起码不会留意料之外的坑。

完整代码

完整的demo代码可以见Github

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容