C++中的Pimpl和RAII惯用法

news/2024/11/23 1:45:10/

一、PImpl 惯用法

PImpl(Pointer to implementation)是一种比较常见的C++编程技巧,采用这种技巧能够减少代码依赖以及编译时间,具体思想是:将类的实现细节(如一些非虚的私有成员)从对象的表示中移除,放到另外的一个类中,并以一个指针(建议是一个独享的指针,如unique_ptr)指向它进行访问。

1.1 Pimpl出现的背景?原因?

在C++中,当头文件中的类定义发生变化,该类所有被使用的地方都需要重新被编译甚至于更改的地方仅仅是外部无法访问的私有成员数据,主要原因在于私有成员数据在以下两个方面会影响一个类:

  • 大小和布局:代码调用者需要知道类的大小和布局(这会包括私有数据成员),换句话说,它始终要能够看到实现,这种约束会导致调用者和被调用者之间存在更紧密的耦合性。当然这是C++对象模型和哲学的核心,因为需要保证编译器默认情况下可以直接访问对象是使C++实现其著名的高度优化效率的要素。
  • 函数:类的私有成员函数也会参与重载决议。

为了减少这写编译依赖,一般会采用指针来隐藏一些实现细节,在C++11中,可以采用如下的Pimpl惯用法写法:

#include<memory>
class A{//一些其他需要放在该处的内容,如虚函数什么的
private:class Impl; // 细节类的前向声明// 采用unique_ptr 是因为Impl的所有权是A对象所独占的,采用该指针包装器可以更好地表达这一意思std::unique_ptr<Impl> impl_ptr; // 指向具体的实现
};

需要提到的是,提到Pimpl惯用法,一般也会提到“编译防火墙”。

被称作编译防火墙的原因在于,采用这种技巧能够很好地避免由于更改部分成员而导致编译级联(多处源文件重新编译)。有一个例外,如果实现类是类模板特化,那么就会丧失编译防火墙的优势:接口的用户必须观测到整个模板定义,以实例化正确的特化。

总的来说,它可带来两个很明显的好处:

  • 编译防火墙,打破编译依赖,省时
  • 隐藏实现细节,即接口与实现分离

1.2 C++11中Pimpl惯用法的最佳实践

前面提到,在C++11及以后的标准,应该尽量避免采用原生的指针(这也是贯彻了RAII的思想)。

以下面的代码为例子:

/*在头文件中*/
class Widget{
public:Widget();~Widget();Widget(Widget &&) noexcept ;Widget& operator= (Widget &&)   noexcept ;Widget(const Widget &) =delete;        // 拷贝构造 定义为删除的Widget& operator=(const Widget&) = delete; // 复制拷贝 定义为删除的
private:class Impl;std::unique_ptr<Impl> impl_ptr;}/*在实现的源文件中*/
class Widget::Impl{int n;// 一些细节
};// 须在外面进行定义 确保Impl 是完整类型
Widget::Widget(int n):impl_ptr(std::make_unique<Impl>(n)){
}Widget::Widget(Widget &&)  noexcept = default;
Widget &Widget::operator=(Widget &&)  noexcept = default;
Widget::~Widget()=default;

在这个例子中,其实也就是这套Pimpl模板中:

  • 采用unique_ptr为了准确表达Widget对象对Impl对象的所有权是独占的,而不是共享的
  • 由于unique_ptr要求指向的类型在任何实例化删除器的语境中均为完整类型,故特殊成员函数需要用户声明且需在Impl定义后定义
  • 构造函数也需要在类外定义并且分配Pimpl对象资源
  • 由于用户自定义了析构函数,所以编译不会生成可移动构造函数和移动赋值运算符(赋值同样),这就需要用户根据需求决定是否提供

额外提一点,建议将所有私有非虚成员移动到具体的实现类中,虚函数需要在继承链中可见,故不建议在Pimpl惯用法将其移动到实现类中。

1.3 一个具体的例子

Widget.h


/********************************************************************************
* @author: Huang Pisong
* @email: huangpisong@foxmail.com
* @date: 2023/8/25 20:48
* @version: 1.0
* @description: 
********************************************************************************/#ifndef TEST_CPP_WORK1_WIDGET_H
#define TEST_CPP_WORK1_WIDGET_H#include <memory>
#include <string>
#include <experimental/propagate_const>class Widget {
public:void draw() const;void draw() ;bool shown() const {return true;}explicit Widget(int n);Widget(Widget &&) noexcept ;Widget& operator= (Widget &&)   noexcept ;Widget(const Widget &) =delete;Widget& operator=(const Widget&) = delete;~Widget();
private:class Impl;  // 前置声明/*propagate_const  会传递const从而保证调用的指针是一致的*/std::experimental::propagate_const<std::unique_ptr<Impl>> impl_ptr;  // 指向实现类的指针};
#endif //TEST_CPP_WORK1_WIDGET_H

Widget.cpp


/********************************************************************************
* @author: Huang Pisong
* @email: huangpisong@foxmail.com
* @date: 2023/8/25 20:48
* @version: 1.0
* @description: 
********************************************************************************/#include <iostream>
#include "Widget.h"// 具体实现
class Widget::Impl{
public:
//    Impl():name("test"),width(0.0),height(0.0){};void draw(const Widget& w) const{if (w.shown())std::cout << "drawing a const component" << "\n";}void draw(const Widget& w){if (w.shown())std::cout << "drawing a non const component" << "\n";}explicit Impl(int n):n(n){};
private:int n;
};void Widget::draw() const {impl_ptr->draw(*this);}
void Widget::draw()  {impl_ptr->draw(*this);}// 需在外面进行定义 确保Impl 是完整类型
Widget::Widget(int n):impl_ptr(std::make_unique<Impl>(n)){
}//
Widget::Widget(Widget &&)  noexcept = default;
Widget &Widget::operator=(Widget &&)  noexcept = default;Widget::~Widget()=default;

二、 RAII惯用法

RAII惯用法的使用能够很好地避免由于手动管理资源带来资源泄漏的问题。

RAII(Resource Acquisition Is Initialization,资源获取即初始化),是一种将必须在使用前请求的资源(如分配的堆内存、执行线程、打开的套接字、打开的文件等)的生命周期与一个对象的生存期相绑定的C++编程技术。

RAII机制保证资源能够用于任何会访问该对象的函数,同时还保证对象在自己生存期结束时会以获取顺序的逆序释放它控制的所有资源。

总的来说,其实就是:

  1. 设计类封装资源(资源绑定对象,生命周期一致性)
  2. 构造函数分配资源
  3. 析构函数销毁资源

cppreference上的一个例子

std::mutex m;void bad() 
{m.lock();                    // 请求互斥体f();                         // 如果 f() 抛出异常,那么互斥体永远不会被释放if(!everything_ok()) return; // 提早返回,互斥体永远不会被释放m.unlock();                  // 只有 bad() 抵达此语句,互斥体才会被释放
}void good()
{std::lock_guard<std::mutex> lk(m); // RAII类:互斥体的请求即是初始化f();                               // 如果 f() 抛出异常,那么就会释放互斥体if(!everything_ok()) return;       // 提早返回也会释放互斥体
}                                      // 如果 good() 正常返回,那么就会释放互斥体

open()/close()lock()/unlock()等就是非RAII类的例子,显然其没有利用到对象的生命周期。

lock_guard是标准库中提供的RAII包装器,用于管理互斥体,在这里使用,可以看到它管理的是std::mutex,当跳出这个函数时,这个资源就会随着lock_guard对象的释放而释放,无需手动去管理。

再举一个例子:

class HeapObjectWrapper{
public:explicit HeapObjectWrapper(int size){if (size <= 0 || size > 1024 * 1024 * 1024)size = 1024;m_p = new char[size];}~HeapObjectWrapper(){delete[] m_p;m_p = nullptr;std::cout << "自动释放资源..."  << "\n";}private:char * m_p;
};int main() {HeapObjectWrapper obj(1024);return 0;
}// 到达这里 申请的资源会被释放

main函数中向堆中申请了1024个堆上的字节,在main函数结束就会调用obj对象的析构函数进行资源的销毁,依然是无需用户手动管理资源,紧紧地跟对象生命周期绑定了。

RAII非常适用于在使用前就需要分配的资源不适用于不会在使用前请求的资源(如CPU时间、核心等)

标准库中也提供了很多包装器来管理用户资源:

  • std::unique_ptr 及 std::shared_ptr 用于管理动态分配的内存,或以用户提供的删除器管理任何以普通指针表示的资源;
  • std::lock_guard、std::unique_lock、std::shared_lock 用于管理互斥体。

参考文章

  • GotW #100: Compilation Firewalls (Difficulty: 6/10) – Sutter’s Mill (herbsutter.com)
  • PImpl - cppreference.com
  • C++编程技巧: Pimpl - 知乎 (zhihu.com)
  • RAII - cppreference.com
  • RAII 惯用法 - Blog (simonzgx.github.io)
  • c++经验之谈一:RAII原理介绍 - 知乎 (zhihu.com)

http://www.ppmy.cn/news/1064566.html

相关文章

如何在windows下使用masm和link对汇编文件进行编译

前言 32位系统带有debug程序&#xff0c;可以进行汇编语言和exe的调试。但真正的汇编编程是“编辑汇编程序文件(.asm)->编译生成obj文件->链接生成exe文件”。下面&#xff0c;我就来说一下如何在windows下使用masm调试&#xff0c;使用link链接。 1、下载相应软件 下载…

vue 孙组件给爷组件传递数据,本质层层传递

1 子传父 例子 vue 子组件 emit传递事件和事件数据给父组件_tenc1239的博客-CSDN博客 2 子传父 父传爷 层层套娃1中例子 2.1 定义传递事件 传值data // 孙组件 methods: {sendDataToGrandpa() {const data hello;this.$emit(sendDataToGrandpa, data);} } 2.2 中间父组件…

【CSS】定位 ( 子元素绝对定位 父元素相对定位 | 代码示例 )

一、子元素绝对定位 父元素相对定位 绝对定位 要和 带有定位 的 父容器 搭配使用 ; 子元素 使用绝对定位 , 父元素要使用 相对定位 ; 子元素使用 绝对定位 , 在布局中不会保留其位置 , 子元素完全依赖 父容器 的位置 , 此时就要求父容器必须稳定 , 如果父容器使用了 绝对布…

算力(Computing Power)

在云计算中&#xff0c;"算力"&#xff08;Computing Power&#xff09;是指计算资源的能力和容量。它是衡量云计算平台、服务或资源提供的计算性能和处理能力的一个关键指标。算力通常包括以下方面的考量&#xff1a; 计算能力&#xff1a; 算力衡量了云计算资源的处…

高并发保证接口幂等性方案

接口幂等的解决方案 什么是接口幂等性 接口幂等性是指无论调用多少次相同的接口请求&#xff0c;对系统的状态和数据产生的影响都是一致的。简而言之&#xff0c;幂等性保证了对同一个接口请求的重复调用不会产生额外的副作用或改变系统的状态。 在设计和实现接口时&#xf…

软件设计师学习笔记3-CPU组成

目录 1.计算机结构 1.1计算机的外设与主机 1.2计算机各部分之间的联系(了解一下即可) 2.CPU结构 1.计算机结构 1.1计算机的外设与主机 1.2计算机各部分之间的联系(了解一下即可) 该图片来自希赛软考 注&#xff1a;黄色的是传递数据的数据总线&#xff0c;白色的是传递控…

【PostGreSQL】PostGreSQL到Oracle的数据迁移

项目需要&#xff0c;有个数据需要导入&#xff0c;拿到手一开始以为是mysql&#xff0c;结果是个PostGreSQL的数据&#xff0c;于是装数据库&#xff0c;但这个也不懂呀&#xff0c;而且本系统用的Oracle&#xff0c;于是得解决迁移转换的问题。 总结下来两个思路。 1、Postg…

Linux 一键启动多个路径的脚本文件

目录 1. 前提条件2. 一键批量启动脚本 前言&#xff1a;为了在linux批量启动程序&#xff0c;把启动和关闭脚本写成脚本文件用于快速启动&#xff0c;我们将用一个脚本来执行这些快速启动的脚本 在linux服务器上一键执行多个脚本文件 1. 前提条件 前提条件是我们已经将单个程…