C++ API暴露最佳实践:如何隐藏依赖库,避免客户端重复编译
我正在开发一款可复用的C++ API,计划在未来项目中使用,当前面临API暴露方式的问题:该API依赖大量第三方库,我不希望客户端重复下载编译这些库。目前我采用如下方案:
Engine.hpp
class __declspec(dllexport) Engine { public: Engine(); ~Engine(); public: void run_window(); // std::vector<UI::Window*> window_list; }; std::string __declspec(dllexport) get_version();
当前该实现可正常运行,但当我尝试加入依赖GLFW的Window类时,客户端因缺少GLFW头文件出现预编译错误。Window类代码如下:
Window.hpp
#include <GLFW/glfw3.h> class Window{ public: Window(std::string name); ... private: GLFWwindow* m_window; };
我原本认为引擎中依赖库的函数应已编译进.dll和.lib文件,但仍出现头文件缺失问题。作为API设计新手,我想了解是否有方法或最佳实践,能在无需客户端引入所有依赖库的情况下暴露依赖库相关代码,同时保留类成员变量供客户端访问。
1. 使用Pimpl(Pointer to Implementation)惯用法
这是C++隐藏实现细节最常用的技巧,核心是把依赖第三方库的代码全部放到.cpp文件和私有实现类中,头文件只对外暴露一个空的实现指针。
修改后的Window.hpp:
// 前向声明实现类,无需包含GLFW头 class WindowImpl; class __declspec(dllexport) Window{ public: Window(std::string name); ~Window(); // 析构函数需在cpp中定义,否则编译器无法销毁WindowImpl // 暴露客户端需要访问的成员函数 std::string get_name() const; int get_width() const; private: WindowImpl* m_impl; // 指向实际实现的指针,客户端看不到内部依赖 };
对应的Window.cpp:
#include "Window.hpp" #include <GLFW/glfw3.h> class WindowImpl { public: WindowImpl(std::string name) { m_window = glfwCreateWindow(800, 600, name.c_str(), nullptr, nullptr); m_name = name; // 其他初始化逻辑 } ~WindowImpl() { glfwDestroyWindow(m_window); } std::string get_name() const { return m_name; } int get_width() const { int w, h; glfwGetWindowSize(m_window, &w, &h); return w; } private: GLFWwindow* m_window; std::string m_name; }; // 对外类的构造/析构及方法实现 Window::Window(std::string name) : m_impl(new WindowImpl(name)) {} Window::~Window() { delete m_impl; } std::string Window::get_name() const { return m_impl->get_name(); } int Window::get_width() const { return m_impl->get_width(); }
这样客户端只需要包含Window.hpp,不需要任何GLFW头文件,所有依赖都被封装在.dll中。
2. 抽象基类(接口)模式
如果需要多态支持,可以定义纯虚基类作为对外接口,实际的GLFW依赖实现放在内部类中,通过工厂方法创建实例。
Window.hpp
class __declspec(dllexport) IWindow { public: virtual ~IWindow() = default; virtual std::string get_name() const = 0; virtual int get_width() const = 0; // 其他纯虚方法 }; // 工厂函数,客户端通过这个获取实例,无需知道具体实现 __declspec(dllexport) IWindow* create_window(const std::string& name); __declspec(dllexport) void destroy_window(IWindow* window);
Window.cpp
#include "Window.hpp" #include <GLFW/glfw3.h> class GLFWWindow : public IWindow { public: GLFWWindow(const std::string& name) { m_window = glfwCreateWindow(800, 600, name.c_str(), nullptr, nullptr); m_name = name; } ~GLFWWindow() override { glfwDestroyWindow(m_window); } std::string get_name() const override { return m_name; } int get_width() const override { int w, h; glfwGetWindowSize(m_window, &w, &h); return w; } private: GLFWwindow* m_window; std::string m_name; }; IWindow* create_window(const std::string& name) { return new GLFWWindow(name); } void destroy_window(IWindow* window) { delete window; }
这种方式不仅隐藏了依赖,还支持后续替换底层窗口库(如SDL)而不影响客户端代码。
3. 核心原则:最小化公开头文件内容
你的问题根源在于公开头文件中包含了第三方库的头,且私有成员使用了第三方库类型——编译器需要知道GLFWwindow的内存布局才能编译Window类,因此客户端必须包含GLFW头。
解决的核心是:公开头文件只保留客户端必须知道的内容,所有依赖第三方库的类型、实现细节全部移到.cpp文件中。
补充注意事项
- 编译.dll时选择静态链接GLFW,将GLFW代码嵌入你的.dll,避免客户端额外部署GLFW的.dll。
- 使用Pimpl时,析构函数不能在头文件默认生成,必须在.cpp中定义,否则编译器因不知道
WindowImpl的大小会报错。 - 若必须在公开头文件中使用第三方库指针,可前向声明该类型(如
struct GLFWwindow;),但客户端无法直接操作该指针,只能通过你的API方法间接访问。
内容的提问来源于stack exchange,提问作者Turgut

