谈谈RAII惯用法-代理对象RAII(下)
转发自公众号文章:谈谈RAII惯用法-代理对象RAII(下)
我们知道汽车除了跑之外,还包括启动、加速、减速、停车等功能,为它们定义相应的成员函数,即:
class car {
public:
void start() {
puts("start car");
}
void stop() {
puts("stop car");
}
void run() {
puts("car is running");
}
void speed_up() {
puts("car is speed up");
}
void speed_down() {
puts("car is speed down");
}
};
如果为了测量car类的每个成员函数的执行时间,需要对每个成员函数都要按照前文的方式使用Aop进行控制,代码如下:
int main() {
car c;
{
Aop aop;
c.start();
}
{
Aop aop;
c.run();
}
{
Aop aop;
c.speed_up();
}
{
Aop aop;
c.speed_down();
}
{
Aop aop;
c.stop();
}
}
显然,所有成员函数的AOP应用套路完全一样,代码也非常简单,每一个成员函数调用时都使用AOP对象来控制,并且借助于{}限定的作用域在函数调用前后位置输出对应的时间日志。虽然编写程序时形式很简单,但是并不优雅,因为每一处成员函数调用都要编写雷同的代码,充满了“坏味道”。
有没有优雅点的方式?
看一下使用AOP的代码片段:
{
Aop aop;
c.run();
}
每一处run()调用时都要对应一个aop对象的创建,为了限定aop的作用域,又得要使用{},显然,如果想办法隐藏掉这个AOP对象,这些重复代码和{}也就不需要了,因此让RAII对象是一个匿名对象,使用匿名表达式RAII可能是一个正确的方向。
我们知道,C++除了有模板技术之外,它还支持操作符重载,并且有一个访问结构成员的操作符->,通过它可以更方便的实现代理模式。因此,基于操作符->重载的机制,方案实现如下:
#include <format> // 使用C++20编译
template<typename T>
class time_log_aop {
// 内部类,作为RAII和代理角色
class proxy_raii {
public:
proxy_raii(T *ptr) : ptr(ptr) {
// 打印输出开始时间
auto now = std::chrono::system_clock::now();
std::cout << std::format("开始时间: {:%Y-%m-%d %H:%M:%S}", now) << std::endl;;
}
~proxy_raii() {
// 打印输出结束时间
auto now = std::chrono::system_clock::now();
std::cout << std::format("结束时间: {:%Y-%m-%d %H:%M:%S}", now) << std::endl;
}
T *operator->() { // 提供->操作符
return ptr;
}
private:
T *ptr;
};
public:
time_log_aop(shared_ptr<T> v) : ptr(move(v)) {}
// 操作符->(),返回proxy_raii对象
proxy_raii operator->() {
return proxy_raii(ptr.get());
}
proxy_raii around() {
return operator->();
}
private:
shared_ptr<T> ptr;
};
先看一下内部类proxy_raii,它在构造时获得开始时间,在析构时获得结束时间,显然是有始有终的操作,并且这对“始、终”组合要分别用在函数调用的前后位置处,这符合RAII机制的应用场景。
此外,它还有一个数据成员T *ptr,和一个返回ptr指针的操作符->重载函数。因为指针的“->”操作具有访问结构成员的能力,而ptr指向一个T类型的对象,因此通过ptr可以访问所指向的T类型对象的成员。proxy_raii包含一个T类型的指针ptr,并且又能通过重载的->操作符来调用ptr的->操作符,也就是它也提供了T类型的所有接口操作,显然可以认为proxy_raii是T类型对象的代理类。
在time_log_aop类中也有一个操作符->重载函数,返回一个proxy_raii对象,显然如果调用time_log_aop::operator->()时,没有接收返回值,此时会返回一个proxy_raii匿名对象,这是前面文章中介绍的匿名对象表达式RAII的应用场景,从而去掉了显式{}。
它的典型应用方式是:
aop->某个函数(); // aop是包装T类型的一个time_log_aop对象
下面看使用场景:
int main() {
time_log_aop aop(make_shared<car>());
aop->run();
aop->speed_up();
aop->speed_down();
}
运行后,输出结果:
开始时间: 2026-03-19 21:16:00.351698338
car is running
结束时间: 2026-03-19 21:16:00.351740722
开始时间: 2026-03-19 21:16:00.351744313
car is speed up
结束时间: 2026-03-19 21:16:00.351746562
开始时间: 2026-03-19 21:16:00.351748559
car is speed down
结束时间: 2026-03-19 21:16:00.351750499
分析一下下面调用语句的工作原理:
aop->run();
aop是time_log_aop类型,当调用它的operator->()操作符时,会创建并返回一个proxy_raii类的临时对象,因为这个proxy_raii类重载了操作符->,所以继续调用它的operator->()操作符,最后返回了aop对象所保存的裸指针(从智能指针对象中获得),该指针最终指向了一个car对象,这样使用operator->()操作符可以访问它所指向的对象的成员函数run()了,这是proxy_raii对象实现的代理功能。
同时又因为调用time_log_aop类的operator->()操作符时,返回的proxy_raii对象是临时对象,而临时对象的特点是具有完整表达式生存期,也就是一旦aop->run()调用运行结束,该临时对象就会被销毁。这个proxy_raii临时对象在创建时进行“开始”的操作,在销毁时进行“结束”的操作,这是proxy_raii对象实现的RAII功能。
因此,该调用语句相当于:
proxy_raii tmp = aop.operator->(); // 输出开始时间
car *p = tmp.operator->();
p->run();
tmp->~proxy_raii(); // 输出结束时间
可见在调用p->run()时,在调用前后分别会有一个打印开始时间和打印结束时间的操作,从而实现了对任意对象的run()成员函数的around方式的AOP操作。同样aop->speed_up()和aop->speed_down()也是如此。
比较一下不使用和使用AOP机制的代码,可见,代码结构和形式完全一样,使用AOP方式时,仿佛就是通过一个car类型的指针来调用它的成员函数:
//创建car对象 创建car代理对象
car *a = new car; time_log_aop aop(make_shared<car>());
//调用成员函数 通过代理调用成员函数
a->run(); aop->run();
a->speed_up(); aop->speed_up();
a->speed_down(); aop->speed_down();
内部类proxy_raii是一个代理类,它代理了外部类的操作,即通过重载操作符->可以访问外部类的所有操作,当然,它并没有直接在每个操作上面进行控制,而是每次代理操作都要通过创建一个临时对象进行,然后在这个临时对象的创建和销毁时进行了控制,比如本例中控制输出了函数执行前后的的开始和结束时间。
此外,proxy_raii类也实现了操作符->的重载,time_log_aop类使用operator->()操作符产生的proxy_raii临时对象接着调用->操作符,从而让->操作符在一个RAII对象的控制下访问结构成员。
结合proxy_raii的特点,它有下面常见的应用场景。
使用场景
1、匿名RAII对象,控制单行函数调用
因为RAII代理对象是匿名的,在这里不妨称之为匿名代理模式。
下面是使用AOP技术访问不同类型对象的run()成员函数的一个函数模板(因为dog和car在“纵向”方向上没有共同的基类,只能使用模板类型来接收dog和car了):
template<typename T>
void test(shared_ptr<T> ptr) {
time_log_aop aop(move(ptr));
aop->run();
}
下面是测试代码:
int main() {
test(make_shared<dog>());
test(make_shared<car>());
}
下面是运行结果:
开始时间: 2026-03-19 21:17:36.913256905
dog is running
结束时间: 2026-03-19 21:17:36.913294896
开始时间: 2026-03-19 21:17:36.913299425
car is running
结束时间: 2026-03-19 21:17:36.913302529
在dog和car对象的run()成员函数时,在调用前和调用后分别输出了开始时间和结束时间的日志信息。
匿名对象是隐式创建的,当然它也可以显式创建,例如:
template<typename T>
void test(shared_ptr<T> ptr) {
time_log_aop aop(move(ptr));
aop.operator->()->run();
}
不过,语句aop.operator->()->run(),这种使用形式并不常见,有点晦涩难懂,阅读代码时不太容易理解,我们可以封装一个普通的成员函数,来用于这个这种场景。
proxy_raii around() {
return operator->();
}
修改为:
template<typename T>
void test(shared_ptr<T> ptr) {
time_log_aop aop(move(ptr));
aop.around()->run();
}
这样更能清晰的表示了是在一个临时对象上面调用了run()等函数。
2、具名RAII对象,控制多行函数调用
RAII代理对象也可以当作具名RAII对象来使用,例如:
int main() {
time_log_aop aop(make_shared<car>());
{
auto raii = aop.operator->();
raii->run();
raii->speed_up();
raii->speed_down();
}
}
运行结果:
开始时间: 2026-03-19 21:18:38.629020332
car is running
car is speed up
car is speed down
结束时间: 2026-03-19 21:18:38.629059963
此时,自动变量raii接收了time_log_aop的操作符->调用返回的proxy_raii对象,并使用{}指定它的生存期作用域,紧接着后面的run,speed_up以及speed_down等成员函数的调用,在这些函数序列调用之前输出开始时间,在调用之后输出结束时间,显然这是常规RAII使用场景。
同样,我们也可以根据operator->()实现的功能,提供一个有名字的普通成员函数,来专门用于这个这种场景,例如前面说的around()成员函数。around()也返回一个proxy_raii对象,可以指定一个对象来接收,此时,RAII代理对象是有名字的,在这里不妨称之为具名代理模式。下面是更常见的使用方式:
int main() {
time_log_aop aop(make_shared<car>());
{
auto raii = aop.around();
raii->run();
raii->speed_up();
raii->speed_down();
}
}
3、AOP操作
前面举的例子的属于AOP环绕方式-around,即在函数调用前后分别进行控制,当然它也支持before和after方式:
如果要求仅支持before时,那就是proxy_raii的构造函数有“开始”的操作,析构函数中不提供“结束”的操作;如果要求仅支持after时,那就是proxy_raii的构造函数没有提供“开始”的操作,析构函数中有“结束”的操作,不再详述。
最后,总结一下这个proxy_raii类的特点:
首先,它实现了RAII机制,可以在构造函数中进行“开始”的操作,在析构函数中进行“结束”的操作。
其次,它是代理对象类,存放了某种目标对象的指针类型的数据成员,并重载了指针访问结构成员的操作符->,也就是它是一个“模拟指针”类,通过->操作符来访问目标对象的成员,进而通过它可以做更多的逻辑处理。
最后,它可以按照匿名对象表达式RAII来使用,也可以作为具名RAII对象来使用,分别用在不同的应用场景。
留一个问题供大家思考:
在C++中,容器类不是线程安全的,如果要对容器,比如vector的成员函数的调用时保证线程安全,需要使用互斥锁进行保护,除了保护单个成员函数的调用外,还有一些多个成员函数一起调用的场景也需要保证线程安全,例如:
if (vec.size() < 100) {
vec.push_back(xxx);
}
结合本文以及前面介绍的RAII使用方式,实现一个线程安全的vector容器类。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)