精读C++设计模式20 —— 结构型设计模式:桥接模式 - 详解

news/2025/10/4 17:46:29/文章来源:https://www.cnblogs.com/lxjshuju/p/19125781

精读C++设计模式20 —— 结构型设计模式:桥接模式

​ 这是我们的第二个设计模式——桥接模式!桥接模式更加直白了,我们之前的适配器更倾向于对接口本身的桥接,这里说的是系统协作的桥接。笔者认为他跟适配器区别谈不上很大。但是还是要仔细说一说这个桥接模式,以及我们下面要引出的,笔者最最常用的pImpl法,他就属于桥接模式的一个响当当的代表

我们在桥接什么?

一句话:我们在桥接具体的抽象和具体的实现。或者说——让本来放在一个类中的实现和接口放在两个类中。举个例子,在过去,我们的类A的接口和实现会写在同一对.cpp/.h中,都隶属于A这个模块的代码。但是现在我们会放到两个类中。接口的类AInterface和因为不同情况而拥有不同实现的AImpl类。

桥接模式的精彩之处在于:将抽象(Abstraction)与实现(Implementor)分离,使两者可以独立变化。换句话说,把「接口/抽象层」和「实现层」分成两个维度,通过在抽象中持有对实现的引用来“桥接”它们。适合那些抽象和实现都可能独立扩展的场景。

这种设计模式被广泛的用在了跨平台库中

​ 太干了,直接甩出来一个例子:

// Implementor(实现接口)
struct DrawingAPI {
virtual ~DrawingAPI() = default;
// 这就是我们的接口
virtual void drawCircle(double x, double y, double r) = 0;
};
// ConcreteImplementorA / B
struct OpenGL_API : DrawingAPI { void drawCircle(...) override { /* OpenGL */ } };
struct DirectX_API : DrawingAPI { void drawCircle(...) override { /* DirectX */ } };
// Abstraction
class Shape {
protected:
std::unique_ptr<DrawingAPI> api_;public:Shape(std::unique_ptr<DrawingAPI> api) : api_(std::move(api)) {}virtual ~Shape() = default;virtual void draw() = 0;};// RefinedAbstractionclass Circle : public Shape {double x_, y_, r_;public:Circle(double x,double y,double r,std::unique_ptr<DrawingAPI> api): Shape(std::move(api)), x_(x), y_(y), r_(r) {}void draw() override { api_->drawCircle(x_, y_, r_); }};

​ 注意到了嘛?我们的Circle到底如何画的,被封在了DirectX_API或者是OpenGL_API,并且可以在不修改 Circle 的情况下新增新 DrawingAPI


PIMPL(Pointer to IMPLementation)

​ PIMPL法太出名了,我甚至想把今天的标题改成设计模式——PIMPL法详谈,但是这属于桥接模式,咱们就讲好了桥接模式,然后再仔细说一说pImpl法是如何工作和设计的。

​ pImpl法没啥稀奇的。当你觉得头文件太膨胀,将实现挪动一个C++编译单元隔离头文件的时候,你实际上就在不自觉的pImpl。但是没有太多,因为我们之间还是再用一个名称符号耦合。如果我们进一步弱化,将实现的细节不光挪到了一个文件中,而是挪到了一个承载了实现细节的IMPL类中,而我们的接口只用一个前置式的声明指针引用,就像这样一样:

class Impl;
class Interface {
public:
Interface() { ... }
void work();
private:
Impl*	impl;
};

​ 我们另开一个文件包含IMPL类的实现和声明,这样就完成了我们的工作。我们实际上以一个非常非常小的代价(一次指针解引用)换来了完全隔离的抽象。

​ 好像还是没啥感觉,那我们从头试试!

当我们不使用pIMPL法

直接在头里声明所有私有数据,编译依赖大,头变动导致大量重编译。

// widget.h
class Widget {
public:
void doWork();
private:
std::vector<int> data_;std::string name_;};

​ 现在你在头文件中添加和减少一点成员,你很快气恼的发现,只要你动了哪怕一点,所有牵扯到的编译单元居然全部都要跟着变一次。如果这个是一个无敌巨大的项目,你一次改动就要重新编译成千上万个源文件!


最简单的 pImpl(裸指针 + 手动析构,缺 copy 支持)

把实现类前向声明,头文件只含指针,定义在 cpp。

// widget.h
class Widget {
public:
Widget();
~Widget();
void doWork();
private:
struct Impl; // forward
Impl* pImpl; // raw pointer
};
// widget.cpp
struct Widget::Impl {
std::vector<int> data;std::string name;void doWorkImpl() { /* ... */ }};Widget::Widget() : pImpl(new Impl{/*init*/}) {}Widget::~Widget() { delete pImpl; }void Widget::doWork() { pImpl->doWorkImpl(); }

​ 最简单的IMPL法!完全可以随意的更改我们的实现了!因为所有的实现和非公开接口的变动,我们的Widget都完全不知道!我们可以随意的迭代我们的Widget::Impl实现!


std::unique_ptr 管理生命周期(推荐起点)

​ 我们是使用现代C++的,用 unique_ptr<Impl> 代替裸指针,自动析构,移动语义更自然。

// widget.h
class Widget {
public:
Widget();
~Widget();
Widget(const Widget&);            // custom copy
Widget& operator=(const Widget&); // custom copy
Widget(Widget&&) noexcept = default;
Widget& operator=(Widget&&) noexcept = default;
void doWork();
private:
struct Impl;
std::unique_ptr<Impl> pImpl;};// widget.cppstruct Widget::Impl { /* same */ };Widget::Widget() : pImpl(std::make_unique<Impl>()) {}Widget::~Widget() = default;Widget::Widget(const Widget& other) : pImpl(std::make_unique<Impl>(*other.pImpl)) {}Widget& Widget::operator=(const Widget& other) {if (this != &other) pImpl = std::make_unique<Impl>(*other.pImpl);return *this;}

引入 clone()(将拷贝逻辑封装到 Impl)

将拷贝逻辑放到 Impl,更灵活,Impl 可以是抽象基类以支持多态 Impl。

struct Widget::Impl {
virtual ~Impl() = default;
virtual std::unique_ptr<Impl> clone() const = 0;virtual void doWorkImpl() = 0;};struct ConcreteImpl : Impl {std::vector<int> data;std::unique_ptr<Impl> clone() const override { return std::make_unique<ConcreteImpl>(*this); }void doWorkImpl() override { /*...*/ }};Widget::Widget(const Widget& other) : pImpl(other.pImpl ? other.pImpl->clone() : nullptr) {}Widget& Widget::operator=(Widget other) { swap(*this, other); return *this; } // copy-and-swap

性能优化:避免不必要的拷贝 —— 支持移动与 noexcept

默认允许移动构造/移动赋值 noexcept,这样容器(如 vector)在扩容时可以用移动而不是拷贝:

Widget(Widget&&) noexcept = default;
Widget& operator=(Widget&&) noexcept = default;

并为拷贝赋值实现 copy-and-swap,以保证强异常安全语义:

friend void swap(Widget& a, Widget& b) noexcept {
using std::swap;
swap(a.pImpl, b.pImpl);
}
Widget& Widget::operator=(Widget other) { swap(*this, other); return *this; }
其他一些乱七八糟收集来的小建议
  1. 在头文件仅 forward declare struct Impl; 并持有 std::unique_ptr<Impl> pImpl;
  2. 在 cpp 中定义 Impl(不暴露在头)。
  3. Impl 提供 clone()(如果需要支持深拷贝)。
  4. 提供 copy ctor/assign 实现为深拷贝(使用 clone),并提供默认或 noexcept 的 move ctor/assign。使用 copy-and-swap 可保证异常安全。
  5. 对性能敏感且发生大量小对象分配时,考虑 SBO;但先用简单方案(unique_ptr + clone)做测量再优化。
  6. 在 API 设计上尽量把修改操作收窄(减少对 Impl 的频繁写操作),以减少开销影响。
  7. 文档标注:pImpl 会阻止部分函数内联(因为实现在 cpp),如需要强内联性能,考虑把该方法放在头部或使用 inline 或模板策略而非 pImpl。
  8. 对于公开类含虚函数的场景:pImpl 不会替代 vtable(虚函数表位于持有虚函数的对象本身),但可以把虚方法的实现委托给 Impl。如果希望把 vtable 也隔离出来,需要另行设计(比如桥接/策略模式)。

代码:一个较为完整的“最佳实践”范例

// widget.h
#include <memory>class Widget {public:Widget();~Widget();Widget(const Widget&);            // deep copy by clone()Widget& operator=(const Widget&); // copy-and-swapWidget(Widget&&) noexcept = default;Widget& operator=(Widget&&) noexcept = default;void doWork();friend void inline swap(Widget& a, Widget& b) noexcept {std::swap;(a.pImpl, b.pImpl);}private:struct Impl;std::unique_ptr<Impl> pImpl;};// widget.cpp#include "widget.h"#include <vector>#include <string>struct Widget::Impl {std::vector<int> data;std::string name;void doWorkImpl() { /* heavy work */ }std::unique_ptr<Impl> clone() const { return std::make_unique<Impl>(*this); }};Widget::Widget() : pImpl(std::make_unique<Impl>()) {}Widget::~Widget() = default;Widget::Widget(const Widget& other) : pImpl(other.pImpl ? other.pImpl->clone() : nullptr) {}Widget& Widget::operator=(const Widget& other) {Widget tmp(other);swap(*this, tmp);return *this;}void Widget::doWork() { pImpl->doWorkImpl(); }

桥接(Bridge) vs 适配器(Adapter)——详细对比(含直观判断准则)

​ 说了这么多,我们回来,对比一下桥接(Bridge)和适配器(Adapter)。

​ 桥接模式,如你看到pIMPL法那样,它的核心工作更集中在:分离抽象与实现,让两边独立扩展。通常是“正面设计”——从一开始就按两个维度设计(例如:抽象类型×实现平台)。适合长期演化、多实现的系统。

​ 适配器更加像是一种补救了,他会把把一个已有接口转换成另一个期望接口,主要用于兼容性或重用。通常是“补救/集成”——把现成类“粘合”到新的接口上——啊哈,这不是补救史山嘛!

总结

我们在解决什么问题

桥接(Bridge)要解决的问题:把“抽象”(接口/上层逻辑)与“实现”(底层细节/平台/策略)分离,让两者能独立演进,避免在单一类层次里把所有组合穷尽(类爆炸)。常见场景:图形库(shape × drawing backend)、跨平台 API、不同策略组合等。

pImpl(作为桥接的代表)要解决的问题:头文件暴露实现细节导致的巨量编译依赖和 ABI 不稳定。目标是把实现移动到编译单元或 Impl 类里,减少头文件变化带来的连锁重编译,同时隐藏第三方/重依赖,提供更好的封装和二进制兼容性。


我们怎么解决的(方法与演进步骤 / 核心模式)

方案对比和收获(优缺点、何时用、实践建议)
  • 裸指针 + 手写析构
    • 优:实现简单、隐藏实现。
    • 缺:内存管理繁琐,拷贝/异常容易出问题。
  • unique_ptr<Impl> + 自定义拷贝(clone)
    • 优:安全、移动高效、拷贝语义明确(深拷贝),推荐默认选项。
    • 缺:每次深拷贝可能有分配开销;阻止部分函数内联。
  • shared_ptr<Impl>(共享) / COW
    • 优:拷贝廉价;读多写少场景有效(COW 可节省复制)。
    • 缺:引用计数开销、多线程下复杂、写时语义增加复杂度。
  • SBO / placement-new
    • 优:减少小对象频繁堆分配开销、提升性能。
    • 缺:实现复杂,调试与维护成本高。只在有测量证据时使用。
干嘛pImpl?
Bridge vs Adapter
  • 意图不同
    • Bridge:从设计一开始就想把抽象和实现分成两个独立维度,面向长期演化。
    • Adapter:事后为兼容或复用而把现有类包一层,转换接口——更多是补救或集成。
  • 实践判别
    • 如果你是为了“允许抽象与实现独立扩展”用 Bridge。
    • 如果你是为了“把已有类适配到新接口”用 Adapter。
  • 外观相似:两者都可能包含委托指针,但关键看设计时机与意图

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/927400.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用纯.NET开发并制作一个智能桌面机器人(六):使用.NET开发一个跨平台功能完善的小智AI客户端

前言 前面几篇文章已经把机器人硬件控制部分的开发讲得差不多了,包括屏幕控制、舵机驱动、语音交互等功能。但是之前的外形太过简单,可动角度不够多,所以我就新改进了一个版本,叫VerdiBot(阿荫),详细视频介绍地…

潍坊网站开发公司wordpress伪原创设置

教程-上传应用公钥并获取支付宝公钥 1. 点击签名验签工具右下角的“上传公钥”会打开支付宝开放平台网页&#xff0c;输入账号登录。&#xff08;建议使用IE或Chrome浏览器。) 2. 在“我的应用”中&#xff0c;选择要配置密钥的应用&#xff0c;点击“查看”。记录对应的APPID…

肥西县住房和城乡建设局网站台州网站制作开发

很久没写过php代码了&#xff0c;二开过程中笔记如下 注意事项 打开APP_DEBUG 关于建表 .在store进行开发&#xff0c;新建表的时候需要加上store_id字段 增加页面 前端页面 增加新的菜单&#xff0c;需要在router.config.js中增加对应的配置增加新的页面&#xff0c;需要…

Gateway-过滤器 - 教程

Gateway-过滤器 - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &quo…

如何构建一个成交型网站响应式网站建设有哪些好处

一个有效的安全事件响应策略的关键组成部分有哪些&#xff1f;一个有效的安全事件响应策略包括四个关键组成部分&#xff0c;它们协同工作以确保对网络安全问题的快速和有效响应。 一个有效的安全事件响应策略的关键组成部分有哪些&#xff1f; 一个有效的安全事件响应策略包括…

公司建站系统企业网盘推荐

文章目录 一、简介二、OS认证三、口令认证四、remote_login_passwordfile 详解 一、简介 在数据库管理中&#xff0c;登录认证是确保数据库安全性的重要环节。Oracle数据库提供 了两种认证方式&#xff0c;一种是“操作系统认证”&#xff0c;一种是“口令文件认证&#xff0c…

深圳网站建设认准乐云犀牛云做网站推广怎么样

概述 在公司项目中使用到 wkhtmltopdf 转换PDF&#xff0c;由于 wkhtmltox-0.12.5 版本 echarts 图形虚线样式&#xff0c;需要升级 wkhtmltox-0.12.6 版本来解决。 官网地址 wkhtmltopdf &#xff1a;https://wkhtmltopdf.org/ windows 安装 下载流程及安装流程 进入官…

RabbitMQ的安装集群、镜像队列部署

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

怎样创办一个网站十堰网络科技公司排名

这是Project Student的一部分。 其他帖子包括带有Jersey的Webservice Client&#xff0c;带有Jersey的 Webservice Server和带有Spring Data的Persistence 。 RESTful Webapp洋葱的第三层是业务层。 这就是应用程序的精髓所在–编写良好的持久性和Web服务层受到约束&#xff0…

单一训练模式适应多个机器人本体 —— skiled brain —— 机器人酷刑现场,竟是为了锻造全能大脑,网友:求AGI饶了我

单一训练模式适应多个机器人本体 —— skiled brain —— 机器人酷刑现场,竟是为了锻造全能大脑,网友:求AGI饶了我地址: https://www.bilibili.com/video/BV1chngzvExC/本博客是博主个人学习时的一些记录,不保证…

2025/10/4 总结

A 用时:1h 预期:100pts 实际:100pts 发现直接暴力的复杂度是调和级数,于是就过了。 总结:对于这种复杂度有有剪枝的题,可以先算时间复杂度看是不是可过。 B 用时:2h 预期:50pts 实际:50pts 已经会了 50pts 暴…

win10界面如何改成经典菜单?

win10界面如何改成经典菜单?win10系统是微软推出的windows操作系统,在一些功能和设置上,与原先的win7有很多不一样的地方,很多小伙伴习惯了win7的开始菜单,开始使用win10的开始菜单非常不习惯,那么win10界面如何…

Qt处理Windows平板上摄像头

Qt处理Windows平板上摄像头pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", …

邢台网站建设服务周到网站域名 英文

grad norm先降后升再降正常嘛 在深度学习中&#xff0c;梯度的范数通常被用来衡量模型参数的更新程度&#xff0c;也就是模型的学习进度。在训练初期&#xff0c;由于模型参数的初始值比较随机&#xff0c;梯度的范数可能会比较大&#xff0c;这是正常现象。随着模型的训练&…

你必须知道的TCP和UDP核心区别,快速搞懂这两大协议!

UDP与TCP详解1. TCP (Transmission Control Protocol) 概念 TCP(传输控制协议)是一种面向连接的、可靠的传输协议。它负责将数据从源主机传输到目标主机,并确保数据的完整性、顺序和正确性。 原理三次握手:在数据传…

机器学习——朴素贝叶斯详解 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

[swift 外部干涉法 extension]

/*** Definition for singly-linked list.* public class ListNode {* public var val: Int* public var next: ListNode?* public init(_ val: Int) {* self.val = val* self.next = ni…

2025国庆Day3

模拟赛 T1 对每个ai开个桶分别算答案即可 注意long long T2 维护m个指针 倒着枚举l p1~pm维护第i个字符已匹配的下标 每次匹配修改一个前缀 复杂度O(n) 另外可考虑:T3 设dpi,j表示i子树内钦定返祖j次的方案数 将相邻向…

量子迁移计划启动:应对未来密码学挑战

荷兰国家安全与网络安全中心发布量子迁移指南,指出量子计算机将在2030-2040年间破解当前主流加密算法,呼吁组织立即行动转向量子安全密码学,保护信息机密性、完整性和可用性。启动量子迁移计划 新闻稿 | 2024年3月2…

找网站公司企业备案搭建微信网站怎么做

使用ArticleColumnForm表单&#xff0c;向数据库提交内容&#xff0c;内容包括column。如果同一用户提交的column重复&#xff0c;则提示表单出错&#xff0c;表单提交失败后&#xff0c;重新渲染表单提交html页面&#xff0c;其中提示错误信息。 涉及的代码包括&#xff1a; …