上篇文章《静态库和链接选项--whole-archive》提到--whole-archive
的一个应用场景:C++自动注册的工厂,这篇文章来填坑。
预备知识
我们经常用工厂类或工厂方法统一管理资源,实现资源的创建和使用之间的解耦,调用者无需关心资源创建的细节,直接到工厂申请创建好的资源即可。
一般情况下,资源提供了统一的接口供使用者调用,到工厂的获取也采用统一的方式,极大地简化了编码,尤其是资源创建比较繁冗的情况下。而且,资源统一管理也从某种程度上节省了资源的重复创建带来的开销。
下面是典型的工厂函数的实现:
using MsgHandler = std::function<bool(std::string_view msg_data)>;MsgHandler*
create_msg_handler(std::string_view msg_type) {if (msg_type == "account") {return http_handle;}else if (msg_type == "pay") {return json_handle;}...else {return nullptr;}
}
这个函数根据不同的消息类型返回对应的消息处理器,基本实现了工厂的注册功能。
我们看下这种实现的问题。假设新增一种消息处理器,我们需要添加一个if语句,然后返回对应的handler,看似问题不大。但只要有修改就会容易出错,甚至不经意的触碰到键盘的一个键都会引入一个bug(这种情况真遇到过),所以我们的原则应该是尽量不要动已有的代码,要从代码结构的扩展性上下手。
自动注册的工厂模式可以解决这个问题。
自动注册的工厂模式
.
├── msg_handler.cpp
├── msg_handler.h
├── pay_handler.cpp
├── CMakeLists.txt
└── main.cpp
其中msg_handler.[cpp|h]
实现工厂,pay_handler.cpp
提供了pay
类型消息的处理器,main.cpp
演示了消息处理器的使用。
// msg_handler.h
#include <functional>
#include <string_view>using MsgHandler = std::function<bool(std::string_view msg_data)>;// 注册消息处理器
void
register_msg_handler(const char *msg_type, MsgHandler handler);// 获取指定消息类型的处理器
MsgHandler*
get_msg_handler(const char *msg_type);// msg_handler.cpp
#include <map>
#include <string>#include "msg_handler.h"static std::map<std::string, MsgHandler>&
get_map() {static std::map<std::string, MsgHandler> map_handlers;return map_handlers;
}void
register_msg_handler(const char *msg_type, MsgHandler handler) {get_map()[msg_type] = handler;
}MsgHandler*
get_msg_handler(const char *msg_type) {auto& m = get_map();auto it = m.find(msg_type);if (it != m.end()) {return &it->second;}else {return nullptr;}
}
msg_handler
实现了简单的工厂,调用register_msg_handler
注册处理器,使用方调用get_msg_handler
获取消息处理器。工厂内部使用map
存储,k为消息类型,v为消息处理器。注意,map_handlers
为函数内部的静态变量,之所以这么做,主要是给这个静态变量确定的初始化顺序:首次调用get_map
时初始化,保证在注册的时候map_handlers
一定是可用的。
下面是pay_handler.cpp
文件,实现了pay
类型的消息处理及自动注册。
#include "msg_handler.h"
#include <stdio.h>class PayHandler {public:PayHandler() { register_msg_handler("pay", PayHandler::handle);}static bool handle(std::string_view msg_data) {printf("pay handle\n");return true;}
};static PayHandler pay_handler;
实现自动注册的逻辑分两步:
PayHandler
构造函数实现消息处理器的注册;声明一个文件作用域的变量,初始化时自动调用构造函数,从而实现了自动注册。
pay_handler
采用全局变量也是可以的,这里加了static
主要为了避免这个变量作用域过大被滥用。
下面是主流程的实现:main.cpp
#include "msg_handler.h"
#include <stdio.h>int main() {MsgHandler* handle = get_msg_handler("pay");if (handle) {(*handle)("test data");}else {printf("not found\n");}return 0;
}
程序正常运行会输出:pay handle
,大家可以先试着编译运行下,看看输出结果。
编译运行
如果没有把pay_handler源代码的目标文件链接到可执行文件,会输出not found
,因为工厂里没有找到对应的handler。
大概率编译的指令跟下面的大同小异,这里只是简单的使用了-l
链接libpayhandler
静态库,然而链接器最终会优化掉,因为通过main.cpp链接器仅能判断出依赖libmsghandler库。
$ g++ -c msg_handler.cpp
$ g++ -c pay_handler.cpp
$ ar rcs libmsghandler.a msg_handler.o
$ ar rcs libpayhandler.a pay_handler.o
$ g++ main.cpp -L. -lpayhandler -lmsghandler -o main
$ ./main
not found
这种情况需要使用--whole-archive
选项强制将库链接进可执行文件,修改为:
g++ main.cpp -L. \-Wl,--whole-archive -lpayhandler \-Wl,--no-whole-archive \-lmsghandler -o main
在这个例子里--whole-archive
是必需的,我们无法像上篇文章那样简单的调整一下main.cpp和静态库的位置解决问题,貌似也没其他方法了。
附上对应的CMakeLists.txt
文件:
cmake_minimum_required (VERSION 3.24.0)
project(main)add_library(payhandler STATIC pay_handler.cpp)
add_library(msghandler STATIC msg_handler.cpp)add_executable(${PROJECT_NAME} main.cpp)target_link_libraries(${PROJECT_NAME}msghandler"$<LINK_LIBRARY:WHOLE_ARCHIVE,payhandler>"
)