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

[C++] 惯用法

1 “拷贝/交换”惯用法(copy-and-swap idiom)

1.1 拷贝赋值

Vector& operator=(const Vector& rhs) {
        Vector(rhs).swap(*this);
        return *this;
    }

在上述代码段中,我们看到的是一个 Vector 类的拷贝赋值运算符(copy assignment operator)的非常规实现方式。这种实现策略,通常被称为“拷贝/交换”惯用法(copy-and-swap idiom),它巧妙地利用了 swap 成员函数来确保资源的安全转移,同时规避了自我赋值问题和潜在的内存泄漏。

不过,需要注意的是,这里的代码实际上更适合作为移动赋值运算符(move assignment operator)的实现基础,而非拷贝赋值运算符。原因在于,它创建了一个 Vector 的临时对象,而该对象是通过 rhs 的拷贝构造函数生成的。如果我们的目标是优化性能,特别是在处理大型数据结构时,那么利用移动语义而非拷贝语义将更为高效。

然而,如果我们暂时忽略性能优化,专注于“拷贝/交换”惯用法的原理,那么这段代码的工作流程如下:

  1. 创建临时对象:首先,通过 Vector(rhs) 调用拷贝构造函数,以 rhs 为模板创建一个 Vector 类型的临时对象。这一步骤涉及资源的复制。

  2. 交换资源:接着,调用 swap(*this) 方法,将当前对象(*this)与刚刚创建的临时对象进行资源交换。swap 函数应确保两个对象在交换后各自拥有对方的资源,且不会引发资源泄漏或悬挂指针等问题。

  3. 销毁临时对象:最后,当临时对象超出作用域时,其析构函数将被自动调用。由于资源已通过 swap 转移给了当前对象,因此临时对象的析构过程将安全且高效地释放不再需要的资源。

  4. 返回当前对象引用:函数返回当前对象的引用(*this),以支持链式赋值操作。

尽管这种“拷贝/交换”策略在概念上清晰且易于理解,但如前所述,在拷贝赋值运算符中实现时,其性能可能并非最优。为了提升性能,我们通常会为移动赋值运算符采用类似的策略,但利用移动构造函数而非拷贝构造函数来创建临时对象。

另外,值得注意的是,如果您的 Vector 类已经包含了有效的 swap 成员函数和移动构造函数,那么实现移动赋值运算符将变得非常简单且高效。例如:

Vector& operator=(Vector rhs) noexcept { // 注意这里rhs是按值传递的,将触发移动构造
    rhs.swap(*this); // 交换当前对象与rhs的资源
    return *this; // 返回当前对象的引用
}

在这个改进的实现中,rhs 是通过按值传递的方式接收的,这将触发移动构造函数(如果编译器和类型支持的话)。随后,通过 swap 函数将资源从 rhs 转移到当前对象,并返回当前对象的引用。这种方法不仅避免了自我赋值问题,还充分利用了移动语义来提升性能。

1.2 移动赋值

    Vector& operator=(Vector&& rhs) {
        Vector(std::move(rhs)).swap(*this);
        return *this;
    }

在上述代码段中,尝试为 Vector 类实现一个移动赋值运算符(move assignment operator)。然而,这种实现方式虽然遵循了“拷贝/交换”惯用法的结构,但应用于移动赋值时却显得不够高效且略显冗余。

问题在于,首先通过 Vector(std::move(rhs)) 创建了一个临时的 Vector 对象,这个对象是通过移动 rhs 来构造的。但紧接着,又调用了 swap 方法来交换当前对象(*this)与这个临时对象之间的资源。实际上,这种操作是多余的,因为您已经通过移动构造得到了 rhs 的资源,接下来只需将这些资源“接管”到当前对象即可,无需再进行交换。

此外,创建临时对象并立即进行交换,不仅增加了不必要的开销(如额外的内存分配和释放),还可能降低代码的可读性和性能。

更高效且直接的实现方式应该是直接“窃取” rhs 的资源,并妥善处理 rhs 留下的“空壳”。这通常意味着将 rhs 的内部指针(如 begin_, end_, end_cap_)直接赋值给当前对象,并将 rhs 的这些指针设置为 nullptr(或采取其他措施确保 rhs 不会释放已转移的资源)。同时,如果 Vector 类包含了一个自定义的分配器(allocator),也需要确保分配器的状态在移动后保持一致。

以下是一个更简洁且高效的移动赋值运算符实现示例:

Vector& operator=(Vector&& rhs) noexcept {
    // 处理自我赋值的情况(可选,但推荐)
    if (this != &rhs) {
        // 释放当前对象持有的资源(如果需要)
        // 例如,如果使用了动态内存分配,这里应该释放它
        // 注意:如果Vector的析构函数会自动处理资源释放,则此步骤可能不是必需的

        // “窃取”rhs的资源
        begin_ = rhs.begin_;
        end_ = rhs.end_;
        end_cap_ = rhs.end_cap_;
        // 如果Vector有一个自定义的allocator成员,也需要移动它
        // allocator_ = std::move(rhs.allocator_);

        // 将rhs的资源指针设置为nullptr(或采取其他措施)
        // 以确保rhs不会释放已转移的资源
        rhs.begin_ = rhs.end_ = rhs.end_cap_ = nullptr;
        // 如果移动了allocator,也需要重置rhs的allocator状态
        // rhs.allocator_ = /* 适当的重置状态 */;

        // 注意:如果Vector的析构函数会自动处理资源释放,
        // 并且allocator的移动构造函数已经正确实现了资源转移,
        // 那么上述对rhs的资源指针和allocator的重置可能是不必要的,
        // 因为rhs的析构函数在调用时应该不会尝试释放已转移的资源。
        // 然而,显式地重置这些指针和状态可以提高代码的可读性和安全性。
    }
    // 返回当前对象的引用以支持链式赋值
    return *this;
}

请注意,上述代码中的注释指出了在处理资源时需要特别注意的几个方面。特别是,如果 Vector 的析构函数会自动处理资源释放(例如,通过删除动态分配的内存),则可能不需要在移动赋值运算符中显式释放这些资源。然而,显式地重置 rhs 的资源指针(如将指针设置为 nullptr)可以提高代码的安全性和可读性,因为它清晰地表明了这些资源已经被转移。

另外,如果 Vector 类包含了一个自定义的分配器(allocator),则需要确保在移动赋值时正确地处理它。这通常意味着在移动构造函数或移动赋值运算符中移动分配器对象,并在必要时重置源对象的分配器状态。然而,如果分配器的移动构造函数已经正确地实现了资源转移,并且析构函数能够安全地处理空或已转移的状态,则可能不需要显式地重置源对象的分配器。


http://www.kler.cn/a/399250.html

相关文章:

  • 第 14 章 -Go语言 错误处理
  • 常用命令之LinuxOracleHivePython
  • 3356. 零数组变换 Ⅱ
  • 丹摩征文活动|丹摩助力selenium实现大麦网抢票
  • 项目配置文件选择(Json,xml,Yaml, INI)
  • [ACTF2020]Upload 1--详细解析
  • 【windows笔记】04-windows下设置端口转发规则(局域网组网实用)
  • 优选算法 - 5 ( 栈 队列 + 宽搜 优先级队列 9000 字详解 )
  • Windows下 TortoiseGit 的使用
  • Python绘制雪花
  • 2.STM32之通信接口《精讲》之USART通信
  • 执行flink sql连接clickhouse库
  • 《线性代数》学习笔记
  • 一个功能强大的文档解析和转换工具,支持PDF、DOCX、PPTX和Markdown等
  • 常用命令之LinuxOracleHivePython
  • 矩阵转置 Matlab与Numpy差异,复数慎重
  • 基于Java Springboot宠物流浪救助系统
  • Android中Crash Debug技巧
  • 单体架构 IM 系统之 Server 节点状态化分析
  • 【Rust中的策略模式实现】
  • 10款PDF合并工具的使用体验与推荐!!!
  • 【Redis】使用redis实现登录校验功能
  • vim配置 --> 在创建的普通用户下
  • linux,一、部署LNMP环境二、配置动静分离三、地址重写四、编写systemd Unit文件
  • Azure pipeline 通过git命令修改文件
  • 记录配置ubuntu18.04下运行ORBSLAM3的ros接口的过程及执行单目imu模式遇到的问题(详细说明防止忘记)