ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++设计模式之代理模式

C++设计模式之代理模式

前言


代理模式(Proxy Pattern)的定义是:为其它对象提供一种代理,以控制对这个对象的访问。这句话读起来抽象,落到代码里其实很具体——客户端本来要直接调用某个对象的方法,现在中间多了一层「代理人」,客户端调用的是代理人,代理人决定什么时候、以什么方式、在什么条件下把调用转给真正的对象。


它解决的问题不是「功能不够」,而是「访问需要被控制」。常见的控制维度有四类:创建时机(对象很贵,用到再建)、访问权限(不是谁都能调用)、位置透明(对象在另一台机器上,但调用方式看起来像本地)、附加动作(调用前后记录日志、计数、加锁)。


初学者最容易把代理和装饰器(Decorator)搞混。两者在代码结构上几乎一样:都继承同一个接口,都持有一个指向真实对象的成员,都在方法里转发调用。区别在意图:装饰器是「给对象增加职责」,可以层层嵌套、每层加一点功能;代理是「控制对对象的访问」,通常自己负责创建或持有真实对象,且不改变被代理对象的行为语义。另一个常见误解是把代理当成「万能的中间层」,在任何调用前都套一层——这会带来额外的间接调用和复杂度,如果一个转发没有任何控制逻辑,它就不该存在。


本文先讲结构与调用时序,再给两种最常用的变体(虚代理、保护代理)的完整代码,最后用表格厘清它与装饰器、适配器、外观的区别。


一、结构与调用时序


代理模式有三个角色:




角色职责



Subject(抽象主题)定义真实对象和代理都实现的接口

RealSubject(真实主题)真正完成业务的对象

Proxy(代理)持有真实对象的引用/指针,实现同一接口,在转发前后插入控制逻辑



调用时序是:客户端持有Subject指针 → 调用request()→ 代理的request()执行控制逻辑 → 必要时创建或定位真实对象 → 转发real->request()。


C++ 里通常用抽象基类 + 虚函数实现接口,用std::unique_ptr管理真实对象的生命周期。也可以不用虚函数,改用模板化的静态代理(编译期绑定),但那样就没法在运行时替换实现,灵活性下降。


代理和真实对象必须实现同一接口,这是模式成立的前提。如果代理暴露的方法签名与真实对象不同,客户端就得知道自己在跟谁打交道,代理的「透明性」也就没了——那更像适配器。


二、常见变体


2.1 虚代理(Virtual Proxy)


用于延迟创建开销大的对象。典型场景是图片浏览器:缩略图列表里可能有上千张图,如果一打开就全部解码,内存和启动时间都受不了。虚代理在构造时只记录路径,第一次真正需要显示时才加载。


关键点是「代理自己负责创建真实对象」,并且只创建一次。这要求成员可变(在 const 方法里也能初始化),通常用mutable std::unique_ptr<RealSubject>。


2.2 保护代理(Protection Proxy)


用于做访问控制。代理在转发前检查调用者权限,无权限时拒绝或抛出异常。这类代理在「核心对象不应该关心权限、权限判断又必须在每次调用前发生」的场景里很有用。


需要强调的是:保护代理只是应用层的访问控制,它不能替代操作系统的权限机制,也不应该被当成安全边界。真正的安全边界在进程、文件系统、数据库权限这些层面。


2.3 远程代理(Remote Proxy)


真实对象在另一个进程或另一台机器上,代理负责序列化参数、发送请求、接收结果,让客户端感觉像本地调用。C++ 里通常配合 gRPC、Thrift 这类框架,手写的话需要处理网络协议、超时、重连,复杂度远超模式本身。


2.4 智能指针代理


std::shared_ptr的operator->返回的是真实对象的指针,std::weak_ptr::lock()在对象存活时返回一个shared_ptr。从「控制访问」的角度看,它们也是代理——在解引用之前先做引用计数或存活检查。这也是代理模式在标准库里最常见的体现,只是很少有人用模式的术语去描述它。


三、与相邻模式的区别




模式接口与被包装对象的关系目的



代理相同通常自己创建/持有真实对象控制访问

装饰器相同由外部传入,可多层嵌套动态增加职责

适配器不同包装一个不兼容接口转换接口

外观简化后的新接口包装多个子系统降低使用复杂度



判断的关键不在代码长什么样,而在意图:同一个类,如果它的职责是「延迟创建 + 缓存」,那是虚代理;如果它的职责是「在原有行为前后加日志」,那是装饰器。两者可以组合,但不要因为结构相似就混用概念。


四、完整示例


4.1 虚代理:延迟加载图片


// 适用于 C++17
#include <iostream>
#include <memory>
#include <string>
#include <utility>

class Image {
public:
virtual ~Image() = default;
virtual void draw() const = 0;
};

class RealImage : public Image {
public:
explicit RealImage(std::string path) : path_(std::move(path)) {
load();
}

void draw() const override {
std::cout << "draw: " << path_ << '\n';
}

private:
void load() {
// 真实场景里这里是解码文件、上传纹理等耗时操作
std::cout << "load: " << path_ << '\n';
}

std::string path_;
};

class ImageProxy : public Image {
public:
explicit ImageProxy(std::string path) : path_(std::move(path)) {}

void draw() const override {
if (!real_) {
real_ = std::make_unique<RealImage>(path_);
}
real_->draw();
}

private:
mutable std::unique_ptr<RealImage> real_; // const 方法里也要能初始化
std::string path_;
};

int main() {
const ImageProxy proxy("photo.png");
std::cout << "proxy constructed\n"; // 此时还没有加载
proxy.draw(); // 第一次调用才加载
proxy.draw(); // 第二次直接复用
return 0;
}

mutable在这里是必要的:draw()是 const 成员函数,但延迟加载需要修改real_。mutable表达的是「这个成员的修改不改变对象的逻辑状态」——对使用者来说,调用draw()前后图片的「内容」没有变化,只是内部缓存被填充了。


4.2 保护代理:按角色控制读写


// 适用于 C++17
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>

enum class Role { Reader, Writer };

class Document {
public:
virtual ~Document() = default;
virtual void read() const = 0;
virtual void write(const std::string& text) = 0;
};

class TextDocument : public Document {
public:
void read() const override {
std::cout << "content: " << content_ << '\n';
}
void write(const std::string& text) override {
content_ = text;
}

private:
std::string content_;
};

class AccessProxy : public Document {
public:
AccessProxy(std::unique_ptr<Document> doc, Role role)
: doc_(std::move(doc)), role_(role) {}

void read() const override {
doc_->read();
}

void write(const std::string& text) override {
if (role_ != Role::Writer) {
throw std::runtime_error("write denied");
}
doc_->write(text);
}

private:
std::unique_ptr<Document> doc_;
Role role_;
};

int main() {
AccessProxy reader(std::make_unique<TextDocument>(), Role::Reader);
reader.read();
try {
reader.write("hello");
} catch (const std::runtime_error& e) {
std::cout << "caught: " << e.what() << '\n';
}

AccessProxy writer(std::make_unique<TextDocument>(), Role::Writer);
writer.write("hello");
writer.read();
return 0;
}

代理持有unique_ptr<Document>,通过基类指针调用虚函数,真实类型可以是任何Document的实现。权限检查集中在write()一处,真实文档类完全不需要知道「谁能写」。


五、代价与注意事项



  • 多一层间接调用。虚函数调用本身有开销,代理又增加了一次转发。在热路径上大量使用代理,可能影响性能;但不要凭空断言「慢多少」,应当用性能分析工具确认瓶颈确实在这里。

  • 生命周期要清楚。代理持有真实对象时,通常用unique_ptr表达独占;如果真实对象由外部管理,代理应当持有引用或shared_ptr,并在文档里写清楚。

  • 线程安全。虚代理的懒加载在单线程下没问题,多线程下两个线程可能同时判断!real_并各自创建一次。需要并发访问时,用std::mutex或std::call_once保护初始化。

  • 不要滥用。如果代理只是原样转发、没有任何控制逻辑,它就是纯粹的负担。每加一层代理,都要能说清楚它控制了什么。


常见坑点


1. 代理与真实对象接口不一致


❌ 代理类只暴露load(),真实对象暴露draw()✅ 两者继承同一个抽象基类,实现完全相同的虚函数签名


接口不一致,客户端就必须知道自己拿的是代理还是真实对象,代理的透明性消失,模式也就名存实亡。


2. 忘记虚析构函数


❌class Image { public: ~Image(); };通过Image*删除派生对象 ✅virtual ~Image() = default;


通过基类指针删除派生类对象而基类析构非虚,是未定义行为,派生类的资源不会释放。


3. 懒加载没有加同步


❌if (!real_) real_ = std::make_unique<RealImage>(path_);在多线程下直接使用 ✅ 用std::mutex保护,或用std::call_once保证只初始化一次


两个线程可能同时判断为「未初始化」,各自创建一个真实对象,其中一个被覆盖泄漏;更糟的是数据竞争本身是未定义行为。


4. 每次调用都重新创建真实对象


❌draw() { RealImage img(path_); img.draw(); }✅ 把创建结果存进成员,后续复用


代理的意义之一是缓存昂贵对象。每次都新建,不仅没有省下开销,还比直接使用真实对象更慢。


5. 用裸指针持有真实对象且不明确所有权


❌Document* doc_;构造时传入new,析构时不管 ✅std::unique_ptr<Document> doc_;或明确约定「不拥有」


所有权不清晰会导致泄漏或重复释放。现代 C++ 应当用智能指针把所有权写进类型。


6. 把代理当成装饰器用


❌ 在代理里加日志、加缓存、加统计,层层嵌套 ✅ 需要动态叠加职责时用装饰器,代理只负责访问控制


结构相似不代表意图相同。把多种职责塞进一个代理,会让它既难测试也难复用。


7. 在 const 方法里修改非 mutable 成员


❌void draw() const { if (!real_) real_ = ...; }而real_未声明mutable✅mutable std::unique_ptr<RealImage> real_;


const 成员函数里不能修改非 mutable 成员,这是编译错误。懒加载代理几乎都需要mutable。


8. 代理对象被拷贝导致重复持有


❌ 代理类可拷贝,拷贝后两个代理各自持有一个unique_ptr副本 ✅ 删除拷贝构造,或改用shared_ptr明确共享语义


unique_ptr本身不可拷贝,如果代理含unique_ptr成员,默认拷贝构造会被删除,这是好事。若确实需要拷贝语义,要显式设计。


总结




要点结论



意图控制对对象的访问,而非增加功能

结构Subject 接口 + RealSubject + Proxy,三者同接口

常见变体虚代理、保护代理、远程代理、智能指针代理

关键实现虚析构、智能指针持有、懒加载加同步

与装饰器结构相同,意图不同:控制访问 vs 增加职责

代价一层间接调用、生命周期与并发复杂度



代理模式在 C++ 里最自然的落地方式,是「抽象接口 +unique_ptr成员 + 虚函数转发」。它适合处理「对象很贵」「权限要先检查」「真实对象在别处」这三类问题。写的时候只要盯住三件事——接口完全一致、析构函数是虚的、所有权用智能指针表达清楚——剩下的就是根据具体场景决定在转发前后插入什么控制逻辑。




RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进