C++ 移动语义、设计模式与 Pimpl 惯用法详解
1. 移动语义
移动语义是 C++11 引入的重要特性,旨在提高性能,尤其是在处理大型对象或临时对象时。以下是关于移动构造函数和移动赋值运算符的详细介绍。
1.1 移动构造函数的实现步骤
- 定义构造函数 :编写一个接受右值引用的构造函数。
Buffer(Buffer&& other)
{
}
- 赋值数据成员 :将右值引用的所有数据成员赋值给当前对象,可在构造函数体或初始化列表中完成。
ptr = other.ptr;
length = other.length;
- 重置右值引用的数据成员 :将右值引用的数据成员赋值为默认值。
other.ptr = nullptr;
other.length = 0;
综合起来,
Buffer
类的移动构造函数如下:
Buffer(Buffer&& other):
{
ptr = other.ptr;
length = other.length;
other.ptr = nullptr;
other.length = 0;
}
1.2 移动赋值运算符的实现步骤
- 定义赋值运算符 :编写一个接受右值引用并返回引用的赋值运算符。
Buffer& operator=(Buffer&& other)
{
}
- 检查是否为同一对象 :避免自我赋值。
if (this != &other)
{
}
- 释放当前对象的资源 :如释放内存。
delete[] ptr;
- 赋值数据成员 :将右值引用的所有数据成员赋值给当前对象。
ptr = other.ptr;
length = other.length;
- 重置右值引用的数据成员 :将右值引用的数据成员赋值为默认值。
other.ptr = nullptr;
other.length = 0;
- 返回当前对象的引用 :无论是否执行了前面的步骤。
return *this;
综合起来,
Buffer
类的移动赋值运算符如下:
Buffer& operator=(Buffer&& other)
{
if (this != &other)
{
delete[] ptr;
ptr = other.ptr;
length = other.length;
other.ptr = nullptr;
other.length = 0;
}
return *this;
}
1.3 移动语义的工作原理
编译器默认提供移动构造函数和移动赋值运算符,除非用户定义了拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符或析构函数。移动语义对于大型对象或不允许拷贝的对象(如
unique_ptr
)具有性能优势。
移动语义主要适用于右值(临时对象),右值没有名称,在表达式求值期间临时存在,并在遇到分号时销毁。在 C++11 中,我们可以通过右值引用(
&&
)来引用这些对象。
移动构造函数和拷贝构造函数的区别在于参数类型,移动构造函数接受右值引用(
T(T&&)
),而拷贝构造函数接受左值引用(
T(T const&)
)。同样,移动赋值运算符接受右值引用(
T& operator=(T&&)
),而拷贝赋值运算符接受左值引用(
T& operator=(T const &)
)。
以下是一个使用移动语义的示例:
std::vector<Buffer> c;
c.push_back(Buffer(100)); // move
Buffer b(200);
c.push_back(b); // copy
c.push_back(std::move(b)); // move
1.4 避免重复代码
可以通过在移动构造函数中调用移动赋值运算符来避免重复代码:
Buffer(Buffer&& other) : ptr(nullptr), length(0)
{
*this = std::move(other);
}
需要注意的是,构造函数的初始化列表中进行成员初始化是必要的,并且需要将
other
静态转换为右值引用。
2. 工厂模式中避免重复的 if…else 语句
在工厂模式中,我们经常会编写重复的
if...else
语句,当选项增多时,代码的可读性和可维护性会降低。可以使用函数映射来避免这种情况。
2.1 准备工作
考虑一个处理不同格式图像文件的系统,定义以下类层次结构:
class Image {};
class BitmapImage : public Image {};
class PngImage : public Image {};
class JpgImage : public Image {};
定义工厂类的接口和典型实现:
struct IImageFactory
{
virtual std::shared_ptr<Image> Create(std::string_view type) = 0;
};
struct ImageFactory : public IImageFactory
{
virtual std::shared_ptr<Image>
Create(std::string_view type) override
{
if (type == "bmp")
return std::make_shared<BitmapImage>();
else if (type == "png")
return std::make_shared<PngImage>();
else if (type == "jpg")
return std::make_shared<JpgImage>();
return nullptr;
}
};
2.2 重构步骤
- 实现工厂接口 :
struct ImageFactory : public IImageFactory
{
virtual
std::shared_ptr<Image> Create(std::string_view type) override
{
// continued with 2. and 3.
}
};
- 定义函数映射 :
static std::map<
std::string,
std::function<std::shared_ptr<Image>()>> mapping
{
{ "bmp", []() {return std::make_shared<BitmapImage>(); } },
{ "png", []() {return std::make_shared<PngImage>(); } },
{ "jpg", []() {return std::make_shared<JpgImage>(); } }
};
- 创建对象 :在映射中查找对象类型,如果找到则使用关联的函数创建对象。
auto it = mapping.find(type.data());
if (it != mapping.end())
return it->second();
return nullptr;
2.3 工作原理
重构后的代码使用了一个
std::map
,键为图像格式的名称,值为创建对象的函数。通过在映射中查找对象类型,避免了重复的
if...else
检查,使工厂的实现更加简单。
以下是使用重构后工厂的示例:
auto factory = ImageFactory{};
auto image = factory.Create("png");
3. Pimpl 惯用法
PIMPL(Pointer to Implementation)惯用法是一种不透明指针技术,用于将类的实现细节与接口分离。
3.1 准备工作
考虑一个表示控件的类,具有文本、大小和可见性等属性,每次属性更改时会重新绘制控件。
class control
{
std::string text;
int width = 0;
int height = 0;
bool visible = true;
void draw()
{
std::cout
<< "control " << std::endl
<< " visible: " << std::boolalpha << visible <<
std::noboolalpha << std::endl
<< " size: " << width << ", " << height << std::endl
<< " text: " << text << std::endl;
}
public:
void set_text(std::string_view t)
{
text = t.data();
draw();
}
void resize(int const w, int const h)
{
width = w;
height = h;
draw();
}
void show()
{
visible = true;
draw();
}
void hide()
{
visible = false;
draw();
}
};
3.2 实现步骤
- 分离私有成员 :将所有私有成员(数据和函数)放入一个单独的类,称为 pimpl 类,原类称为公共类。
- 前向声明 :在公共类的头文件中对 pimpl 类进行前向声明。
// in control.h
class control_pimpl;
-
声明指针
:在公共类定义中使用
unique_ptr声明一个指向 pimpl 类的指针。
class control
{
std::unique_ptr<
control_pimpl, void(*)(control_pimpl*)> pimpl;
public:
control();
void set_text(std::string_view text);
void resize(int const w, int const h);
void show();
void hide();
};
- 定义 pimpl 类 :在公共类的源文件中定义 pimpl 类,该类镜像公共类的公共接口。
// in control.cpp
class control_pimpl
{
std::string text;
int width = 0;
int height = 0;
bool visible = true;
void draw()
{
std::cout
<< "control " << std::endl
<< " visible: " << std::boolalpha << visible
<< std::noboolalpha << std::endl
<< " size: " << width << ", " << height << std::endl
<< " text: " << text << std::endl;
}
public:
void set_text(std::string_view t)
{
text = t.data();
draw();
}
void resize(int const w, int const h)
{
width = w;
height = h;
draw();
}
void show()
{
visible = true;
draw();
}
void hide()
{
visible = false;
draw();
}
};
- 实例化 pimpl 类 :在公共类的构造函数中实例化 pimpl 类。
control::control() :
pimpl(new control_pimpl(),
[](control_pimpl* pimpl) {delete pimpl; })
{}
- 调用 pimpl 类的成员函数 :公共类的成员函数调用 pimpl 类的相应成员函数。
void control::set_text(std::string_view text)
{
pimpl->set_text(text);
}
void control::resize(int const w, int const h)
{
pimpl->resize(w, h);
}
void control::show()
{
pimpl->show();
}
void control::hide()
{
pimpl->hide();
}
3.3 工作原理
Pimpl 惯用法的主要优点是隐藏类的内部实现细节,提供干净的接口,实现二进制向后兼容,减少编译时间。但也存在代码量增加、可读性降低、运行时开销等缺点。
在实现 Pimpl 惯用法时,需要注意将所有非虚拟的私有成员数据和函数放入 pimpl 类,而将受保护的数据成员、函数和所有私有虚拟函数留在公共类中。
总结
本文介绍了 C++ 中的移动语义、工厂模式中避免重复
if...else
语句的方法以及 Pimpl 惯用法。移动语义可以提高性能,尤其是在处理大型对象和临时对象时;使用函数映射可以避免工厂模式中的重复代码;Pimpl 惯用法可以将类的实现细节与接口分离,提供更好的可维护性和二进制兼容性。通过合理运用这些技术,可以编写更高效、更易读和更健壮的 C++ 代码。
展望
在实际应用中,可以根据具体需求进一步优化这些技术的使用。例如,在移动语义中,可以根据对象的特点选择更合适的移动策略;在工厂模式中,可以考虑使用更复杂的映射结构来处理不同的创建需求;在 Pimpl 惯用法中,可以探索更灵活的实现方式,以适应不同的类设计。同时,还可以结合其他 C++ 特性,如模板、智能指针等,进一步提升代码的质量和性能。
流程图
graph TD;
A[开始] --> B[移动语义];
B --> B1[移动构造函数];
B --> B2[移动赋值运算符];
A --> C[工厂模式];
C --> C1[避免重复if...else];
A --> D[Pimpl惯用法];
D --> D1[分离私有成员];
D --> D2[前向声明];
D --> D3[声明指针];
D --> D4[定义pimpl类];
D --> D5[实例化pimpl类];
D --> D6[调用pimpl类成员函数];
B1 --> E[性能提升];
B2 --> E;
C1 --> F[代码简洁];
D6 --> G[接口与实现分离];
E --> H[高效代码];
F --> H;
G --> H;
H --> I[结束];
表格
| 技术 | 优点 | 缺点 |
|---|---|---|
| 移动语义 | 提高性能,处理大型对象和临时对象 | |
| 工厂模式(函数映射) | 避免重复代码,提高可读性和可维护性 | 需要额外的内存来存储映射 |
| Pimpl 惯用法 | 隐藏实现细节,提供干净接口,实现二进制向后兼容,减少编译时间 | 代码量增加,可读性降低,运行时开销 |
C++ 移动语义、设计模式与 Pimpl 惯用法详解
4. 不同技术的对比与应用场景
为了更好地理解移动语义、工厂模式中避免重复
if...else
语句以及 Pimpl 惯用法,下面通过表格对比它们的特点和适用场景:
| 技术 | 特点 | 适用场景 |
| ---- | ---- | ---- |
| 移动语义 | 提高性能,尤其是处理大型对象或临时对象;区分左值和右值操作 | 当需要频繁处理临时对象或大型对象,且希望避免不必要的拷贝时,如容器操作、智能指针管理等 |
| 工厂模式(函数映射) | 避免重复的
if...else
语句,提高代码的可读性和可维护性;使用函数映射简化对象创建逻辑 | 当需要根据不同条件创建不同类型的对象,且条件较多时,如创建不同格式的图像、文件等 |
| Pimpl 惯用法 | 分离类的实现细节和接口,提供干净的接口;实现二进制向后兼容,减少编译时间 | 当需要隐藏类的内部实现,避免因实现细节的改变而影响客户端代码时,如开发库、框架等 |
5. 实际案例分析
5.1 移动语义在容器操作中的应用
在容器操作中,移动语义可以显著提高性能。例如,在向
std::vector
中添加元素时,如果使用移动语义,可以避免不必要的拷贝。
#include <iostream>
#include <vector>
class LargeObject {
public:
LargeObject() {
data = new int[1000000];
std::cout << "Constructor" << std::endl;
}
LargeObject(const LargeObject& other) {
data = new int[1000000];
for (int i = 0; i < 1000000; ++i) {
data[i] = other.data[i];
}
std::cout << "Copy Constructor" << std::endl;
}
LargeObject(LargeObject&& other) noexcept {
data = other.data;
other.data = nullptr;
std::cout << "Move Constructor" << std::endl;
}
~LargeObject() {
delete[] data;
}
private:
int* data;
};
int main() {
std::vector<LargeObject> vec;
vec.push_back(LargeObject()); // 使用移动语义
return 0;
}
在上述代码中,
push_back(LargeObject())
使用了移动语义,避免了拷贝构造函数的调用,提高了性能。
5.2 工厂模式在图像创建中的应用
在处理不同格式的图像时,使用工厂模式可以避免重复的
if...else
语句,使代码更加简洁和可维护。
#include <iostream>
#include <memory>
#include <map>
#include <string>
#include <functional>
class Image {
public:
virtual void display() = 0;
virtual ~Image() {}
};
class BitmapImage : public Image {
public:
void display() override {
std::cout << "Displaying Bitmap Image" << std::endl;
}
};
class PngImage : public Image {
public:
void display() override {
std::cout << "Displaying PNG Image" << std::endl;
}
};
class JpgImage : public Image {
public:
void display() override {
std::cout << "Displaying JPG Image" << std::endl;
}
};
struct IImageFactory {
virtual std::shared_ptr<Image> Create(std::string_view type) = 0;
virtual ~IImageFactory() {}
};
struct ImageFactory : public IImageFactory {
std::shared_ptr<Image> Create(std::string_view type) override {
static std::map<
std::string,
std::function<std::shared_ptr<Image>()>> mapping
{
{ "bmp", []() {return std::make_shared<BitmapImage>(); } },
{ "png", []() {return std::make_shared<PngImage>(); } },
{ "jpg", []() {return std::make_shared<JpgImage>(); } }
};
auto it = mapping.find(std::string(type));
if (it != mapping.end()) {
return it->second();
}
return nullptr;
}
};
int main() {
auto factory = ImageFactory{};
auto image = factory.Create("png");
if (image) {
image->display();
}
return 0;
}
在上述代码中,通过函数映射实现了工厂模式,根据不同的图像格式创建相应的图像对象。
5.3 Pimpl 惯用法在库开发中的应用
在开发库时,使用 Pimpl 惯用法可以隐藏库的内部实现细节,提供干净的接口,使库的更新更加方便。
// control.h
#pragma once
#include <memory>
#include <string_view>
class control_pimpl;
class control {
std::unique_ptr<
control_pimpl, void(*)(control_pimpl*)> pimpl;
public:
control();
void set_text(std::string_view text);
void resize(int const w, int const h);
void show();
void hide();
~control();
};
// control.cpp
#include "control.h"
#include <iostream>
class control_pimpl {
std::string text;
int width = 0;
int height = 0;
bool visible = true;
void draw() {
std::cout
<< "control " << std::endl
<< " visible: " << std::boolalpha << visible <<
std::noboolalpha << std::endl
<< " size: " << width << ", " << height << std::endl
<< " text: " << text << std::endl;
}
public:
void set_text(std::string_view t) {
text = t.data();
draw();
}
void resize(int const w, int const h) {
width = w;
height = h;
draw();
}
void show() {
visible = true;
draw();
}
void hide() {
visible = false;
draw();
}
};
control::control() :
pimpl(new control_pimpl(),
[](control_pimpl* pimpl) {delete pimpl; })
{}
void control::set_text(std::string_view text) {
pimpl->set_text(text);
}
void control::resize(int const w, int const h) {
pimpl->resize(w, h);
}
void control::show() {
pimpl->show();
}
void control::hide() {
pimpl->hide();
}
control::~control() = default;
// main.cpp
#include "control.h"
int main() {
control c;
c.set_text("Hello, World!");
c.resize(100, 200);
c.show();
c.hide();
return 0;
}
在上述代码中,通过 Pimpl 惯用法将
control
类的实现细节隐藏在
control_pimpl
类中,客户端代码只需要包含
control.h
头文件,不需要关心内部实现。
6. 总结与建议
通过对移动语义、工厂模式中避免重复
if...else
语句以及 Pimpl 惯用法的介绍和分析,我们可以得出以下总结和建议:
-
移动语义
:在处理大型对象和临时对象时,优先考虑使用移动语义,可以显著提高性能。同时,注意在移动构造函数和移动赋值运算符中正确处理资源的转移。
-
工厂模式(函数映射)
:当需要根据不同条件创建不同类型的对象时,使用函数映射可以避免重复的
if...else
语句,提高代码的可读性和可维护性。但要注意映射结构的内存开销。
-
Pimpl 惯用法
:在开发库、框架等需要隐藏内部实现细节的场景中,使用 Pimpl 惯用法可以实现二进制向后兼容,减少编译时间。但要注意代码量的增加和可读性的降低。
流程图
graph TD;
A[实际应用场景] --> B[容器操作];
A --> C[图像创建];
A --> D[库开发];
B --> B1[移动语义应用];
C --> C1[工厂模式应用];
D --> D1[Pimpl惯用法应用];
B1 --> E[性能提升];
C1 --> F[代码简洁];
D1 --> G[接口与实现分离];
E --> H[高效代码];
F --> H;
G --> H;
H --> I[实际效果验证];
I --> J[持续优化];
表格
| 实际案例 | 技术应用 | 效果 |
|---|---|---|
| 容器操作 | 移动语义 | 避免不必要的拷贝,提高性能 |
| 图像创建 | 工厂模式(函数映射) |
避免重复
if...else
语句,提高代码可读性和可维护性
|
| 库开发 | Pimpl 惯用法 | 隐藏内部实现细节,实现二进制向后兼容,减少编译时间 |
超级会员免费看
979

被折叠的 条评论
为什么被折叠?



