适配器模式在c++中应优先用组合(持有adaptee成员)而非继承,公开目标接口并封装调用逻辑;避免模板泛化、隐藏被适配对象、构造时重初始化;注意非standard-layout导致offsetof/memcpy未定义;需显式定义移动语义;std::function仅适用于无状态轻量转换。

适配器模式在 C++ 里怎么写才不绕弯子
直接说结论:用类继承或组合 + 公开目标接口,把已有类“包一层”,让它符合新接口。别搞虚函数表重排、别硬套 UML 图,先让代码跑通再谈设计。
常见错误是把 Adapter 写成模板万能胶,结果每个调用都得推导类型,编译慢还报一堆 no matching function;或者把被适配对象藏太深,调试时根本看不出实际调用的是哪个 adaptee.do_something()。
- 优先用**组合**(持有
Adaptee成员),比继承更安全、更易测试 - 如果
Adaptee是 final 类或你不能修改它,继承不可行,只能组合 - 不要在
Adapter构造函数里做重资源初始化(比如打开文件、连网络),容易让使用者误以为构造廉价 - 若
Adaptee接口有 const 成员函数但目标接口没声明 const,记得在Adapter对应函数后加const,否则无法绑定 const 对象
示例:
class Adaptee {
public:
int specific_request() { return 42; }
};
class Target {
public:
virtual int request() = 0;
virtual ~Target() = default;
};
class Adapter : public Target {
Adaptee adaptee_;
public:
int request() override { return adaptee_.specific_request() + 1; }
};
类对象内存布局变了吗?会影响 offsetof 或 memcpy 吗
会变,而且很容易踩坑——尤其当你把适配器当 POD 类用,或者想用 memcpy 拷贝对象时。
立即学习“C++免费学习笔记(深入)”;
组合方式的 Adapter 一般只是在原有 Adaptee 前/后加了虚表指针(如果有虚函数)或 padding,offsetof 到成员的偏移不再是原 Adaptee 的值;继承方式则可能因虚继承引入额外指针,彻底打乱布局。
- 只要类里有虚函数(哪怕只有
~Target()),就不是标准布局(standard-layout),offsetof对非静态数据成员的结果未定义 - 别对
Adapter对象用memcpy,虚表指针不会被正确复制,运行时大概率 crash - 如果真要二进制兼容,用
std::is_standard_layout_v<t></t>编译期检查,不通过就别碰memcpy和reinterpret_cast - 结构体嵌套场景下,别假设
sizeof(Adapter)==sizeof(Adaptee) + sizeof(padding),编译器对齐策略可能出人意料
为什么 move 构造/赋值在适配器里经常失效
因为默认生成的移动操作只移动成员,而你手动写的 Adapter 如果没显式定义移动语义,编译器会禁用移动(退化为拷贝),或者移动后 Adaptee 成员状态未清理,导致二次析构。
典型现象:容器 push_back(std::move(adapter)) 后原对象仍可调用 request(),且返回垃圾值。
- 如果
Adaptee支持移动(有移动构造/赋值),Adapter必须显式委托:用adaptee_(std::move(other.adaptee_)) - 虚析构函数不影响移动,但若
Adaptee移动后进入无效状态,Adapter的析构函数必须能安全处理(比如检查空指针) - 别依赖编译器自动生成移动函数——一旦类里新增一个 non-moveable 成员(如
std::mutex),整个类自动失去移动能力,且无编译警告
std::function 能替代适配器类吗?什么情况下不该用
能,但只适合一次性、轻量级的接口转换;一旦需要保存状态、复用逻辑、或和多态容器交互,std::function 就开始拖后腿。
常见误用:用 std::function<int></int> 包一层 adaptee.specific_request,然后塞进 std::vector<:function>></:function> ——看似简洁,实则每次调用都有间接跳转开销,且无法 downcast 回原始类型。
-
std::function对象大小通常是 24–32 字节(取决于实现),比裸指针大得多,缓存不友好 - 无法获取被包装对象地址,调试时看不到实际调用栈源头
- 如果适配逻辑涉及多个成员访问(比如先查缓存再调用
Adaptee),闭包捕获会增加生命周期管理复杂度 - 跨 DLL 边界传递
std::function有 ABI 风险,尤其 MSVC 和 GCC 混用时
该用类的时候就老老实实用类,别为了少写几行 class 关键字,换来一堆隐式拷贝和性能盲点。











