当前位置: 首页 > news >正文

C++基础: Rule of five/zero/three

Rule of five

在 C++ Core Guidelines 中,有这样的一条指导原则:

C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all

就是说如果你定义了或者删除(=delete): 拷贝构造函数, 移动构造函数, 拷贝赋值函数, 移动赋值函数, 析构函数中的任意一个函数, 那么你需要定义或者删除它们全部. 注意: 普通的构造函数不在其中.

在通常情况下, 编译器会尝试为用户生成上面的 5 个函数. 编译器生成的函数的行为:

  • 拷贝构造函数:按顺序拷贝构造每个成员变量
  • 拷贝赋值运算符:按顺序拷贝赋值每个成员变量
  • 移动构造函数:按顺序移动构造每个成员变量
  • 移动赋值运算符:按顺序移动赋值每个成员变量
  • 析构函数:按逆序销毁每个成员变量

但是如果用户定义了其中之一, 那么编译器的行为就会发生变化. 具体如下:

ruleoffive

读者很难记住这个表格中的所有信息, 所以这会让使用者产生困惑. 为了让读者有个深切的体验, 后面我将举一个例子, 带大家一步步了解.

Simple String

我们以实现一个简单的字符串类为例:

  1. 因为我们在构造的时候分配了存储空间, 所以需要加一个析构函数释放对应存储.

    struct SString {
      SString(char const* cp) : data_(new char[strlen(cp) + 1]) {
        strcpy(data_, cp);
      }
      ~SString() { delete[] data_; }
    
    private:
      char* data_;
    };
    
  2. 如果现在有这样一个函数, 使用传值方式调用, 那该如何?

    void fun(SString val) {
        //...
    }
    

    这个时候因为编译器生成的拷贝函数会按位拷贝, 也就是两个实例的data_指针指向同一个地址, 这样会造成调用两次析构函数, 有重复释放的问题. 因此我们此时需要修改拷贝构造函数.
    于是代码进一步变成这样:

    struct SString {
      SString(char const* cp) : data_(new char[strlen(cp) + 1]) {
        strcpy(data_, cp);
      }
      ~SString() { delete[] data_; }
      SString(SString const& rhs) : data_(new char[strlen(rhs.data_) + 1]) {
        strcpy(data_, rhs.data_);
      }
    
    private:
      char* data_;
    };
    
  3. 这还没完, 如果此时有一个赋值语句, 程序就会出问题.

    SString src{"I’m going to be copied"};
    SString dst{"I have a value"};
    // …
    dst = src;
    

    此时我们需要自定义一个拷贝赋值函数. 不能使用默认的拷贝赋值.

    #include <cstdlib>
    #include <cstring>
    #include <utility>
    
    struct SString {
      SString(char const* cp) : data_(new char[strlen(cp) + 1]) {
        strcpy(data_, cp);
      }
      ~SString() { delete[] data_; }
      SString(SString const& rhs) : data_(new char[strlen(rhs.data_) + 1]) {
        strcpy(data_, rhs.data_);
      }
      SString& operator=(SString const& rhs) {
        char* newdata = new char[strlen(rhs.data_) + 1];
        strcpy(newdata, rhs.data_);
        std::swap(newdata, data_);
        delete[] newdata;
        return *this;
      }
    
    private:
      char* data_;
    };
    
    void fun(SString val) {
      //...
    }
    
    int main() {
      SString s("Hello, World!");
      fun(s);
    
      SString src{"I’m going to be copied"};
      SString dst{"I have a value"};
      // …
      dst = src;
    
      return 0;
    }
    
  4. 此时的代码还有问题. 下面的情况下我们希望调用移动构造函数, 但是实际上会调用拷贝构造函数. 加个打印语句可以验证这一点.

    SString s{"I'm temporary"};
    fun(std::move(s));
    

    为了正确支持 move, 我们需要定义一个移动构造函数.

    struct SString {
      SString(char const* cp) : data_(new char[strlen(cp) + 1]) {
        strcpy(data_, cp);
      }
      ~SString() { delete[] data_; }
      SString(SString const& rhs) : data_(new char[strlen(rhs.data_) + 1]) {
        std::cout << "copy constructor" << std::endl;
        strcpy(data_, rhs.data_);
      }
      SString& operator=(SString const& rhs) {
        std::cout << "copy assign" << std::endl;
        char* newdata = new char[strlen(rhs.data_) + 1];
        strcpy(newdata, rhs.data_);
        std::swap(newdata, data_);
        delete[] newdata;
        return *this;
      }
      SString(SString&& rhs) noexcept : data_(rhs.data_) {
        rhs.data_ = nullptr;
        std::cout << "move constructor" << std::endl;
      }
    
    private:
      char* data_;
    };
    
  5. 最后一个需要面对的场景就是移动赋值. 加上打印语句可以看出来当前的移动赋值调用了拷贝赋值函数.

    SString src{"hello"};
    SString dst{"world"};
    dst = std::move(src);
    

    需要实现一个移动赋值函数.

    struct SString {
      SString(char const* cp) : data_(new char[strlen(cp) + 1]) {
        strcpy(data_, cp);
      }
      ~SString() { delete[] data_; }
      SString(SString const& rhs) : data_(new char[strlen(rhs.data_) + 1]) {
        std::cout << "copy constructor" << std::endl;
        strcpy(data_, rhs.data_);
      }
      SString& operator=(SString const& rhs) {
        std::cout << "copy assign" << std::endl;
        char* newdata = new char[strlen(rhs.data_) + 1];
        strcpy(newdata, rhs.data_);
        std::swap(newdata, data_);
        delete[] newdata;
        return *this;
      }
      SString(SString&& rhs) noexcept : data_(rhs.data_) {
        rhs.data_ = nullptr;
        std::cout << "move constructor" << std::endl;
      }
    
      SString& operator=(SString&& rhs) {
        std::cout << "move assign" << std::endl;
        delete[] data_;
        data_ = rhs.data_;
        rhs.data_ = nullptr;
        return *this;
      }
    
    private:
      char* data_;
    };
    

所以, 为了一个简单的类, 我们需要定义 5 个函数. 能避免还是要尽量避免这样做.

Rule of zero

在 C++ Core Guidelines 中, C.20这样写:

C.20: If you can avoid defining any default operations, do

C.20的意思是: 如果你可以避免定义任何默认操作, 那么你就应该避免.

对上面的简单字符串类来说, 如果你只是扩展功能, 那么完全不用自己管理空间, 托管给std::string(或者其他 RAII 类)即可.

struct EString {
  EString(char const * cp) : data_(cp) {}
  std::string data_;
};

Rule of three

在 C++11 之前, 没有移动构造和移动赋值函数, 所以减去这两个就剩 3 个函数了. 也就是他们基于类似的考虑, 但是数量不一样而已.

总结

实际工作中如果有能用的工具类尽量使用工具类(Rule of zero), 当然如果你是那个要造轮子的人, 那么请遵从 Rule of Five 实现全部必要的函数.

相关文章:

  • 机器学习中的分布统计量:从理论到应用
  • 【大模型】Token计算方式与DeepSeek输出速率测试
  • 单片机开发资源分析的实战——以STM32F103C8T6为例子的单片机资源分析
  • 机器学习中矩阵求导公式
  • ubuntu 根据src 包从新打包
  • 209. 长度最小的子数组
  • 【git】git管理规范--分支命名规范、CommitMessage规范
  • dockercompose如何重启单个服务和所有服务
  • 计算机网络笔记(四)——1.4计算机网络在我国的发展
  • FreeRTOSBug解析:一个任务printf打印一半突然跳转另一个任务,导致另一个任务无法打印
  • 深入探索Spring Boot 配置文件:类型、加载顺序与最佳实践
  • python中使用单例模式在整个程序中只创建一个数据库连接,节省资源
  • DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能示例2,TableView15_02导出为CSV格式示例
  • PostgreSQL:简介与安装部署
  • Midjourney使用教程—1.提示词基础知识
  • Springboot中的@ConditionalOnBean注解:使用指南与最佳实践
  • 使用PyMongo操作MongoDB(二)
  • 传奇怪物素材 8方向高清怪物 PNG格式 游戏怪物 14组
  • Android Fresco 框架工具与测试模块源码深度剖析(五)
  • 超图神经网络的详细解析与python示例
  • 重庆城市轨道交通拟听证调价:公布两套票价方案,正征求意见
  • 江苏疾控:下设部门无“病毒研究所”,常荣山非本单位工作人员
  • 以军称已开始在加沙的新一轮大规模攻势
  • 病愈出院、跳大神消灾也办酒,新华每日电讯:农村滥办酒席何时休
  • 《日出》华丽的悲凉,何赛飞和赵文瑄演绎出来了
  • 大环线呼之欲出,“金三角”跑起来了