34、C++ 移动语义、设计模式与 Pimpl 惯用法详解

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 惯用法 隐藏内部实现细节,实现二进制向后兼容,减少编译时间
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值