文章

条款17:理解特殊成员函数的生成

了解何时生成拷贝/移动/析构函数及其相互影响,合理用 =default。

条款17:理解特殊成员函数的生成

条款17:理解特殊成员函数的生成

什么是特殊成员函数?

特殊成员函数是指 编译器自动生成 的成员函数,主要包括:

  • 默认构造函数(Default Constructor)
  • 析构函数(Destructor)
  • 拷贝构造函数(Copy Constructor)
  • 拷贝赋值运算符(Copy Assignment Operator)

C++11 新增:

  • 移动构造函数(Move Constructor)
  • 移动赋值运算符(Move Assignment Operator)

只有当代码需要调用这些函数,但又没有显式声明时,编译器才会自动生成。

自动生成的规则和特征

  • 自动生成的函数默认是 publicinline
  • 除析构函数外,它们通常是非虚函数。
  • 只有在类未显式声明对应函数时才生成。
  • 默认构造函数仅在类无任何用户声明构造函数时才生成。

移动构造函数和移动赋值运算符

函数签名示例

1
2
3
4
5
class Widget {
public:
    Widget(Widget&& rhs);            // 移动构造函数
    Widget& operator=(Widget&& rhs); // 移动赋值运算符
};
  • 移动操作会对类的非静态成员和基类部分调用对应成员的移动操作。
  • 如果成员类型不支持移动,退化为拷贝。
  • 声明一个移动操作,会阻止编译器生成另一个移动操作。

拷贝操作的规则

  • 拷贝构造和拷贝赋值是相互独立的。
  • 声明一个不会阻止另一个自动生成。

移动和拷贝操作的交互规则

  • 声明拷贝操作会阻止移动操作的自动生成。
  • 声明移动操作会阻止拷贝操作的自动生成(编译器将其设置为 delete)。

Rule of Three(三法则)

  • 如果声明了拷贝构造、拷贝赋值或析构函数之一,应声明另外两个。
  • 目的是保证资源管理正确(内存、文件等)。

移动操作生成的条件

编译器只会在满足下列条件时生成移动构造和移动赋值:

  • 未声明拷贝构造函数、拷贝赋值运算符
  • 未声明移动构造函数、移动赋值运算符
  • 未声明析构函数

析构函数与移动操作

声明析构函数会阻止移动操作生成,但不会阻止拷贝操作生成。

这可能导致性能问题,因为:

  • 移动请求被转化为拷贝操作。
  • 例如包含大量数据的容器类,拷贝比移动慢很多。

解决办法:显式声明并默认实现移动和拷贝操作。

示例详解

用户声明析构函数,阻止移动生成

1
2
3
4
5
6
7
8
9
10
11
12
class StringTable {
public:
    StringTable() { /* 构造函数 */ }

    ~StringTable() { 
        // 自定义析构,阻止移动操作自动生成
        makeLogEntry("Destroying StringTable object");
    }

private:
    std::map<int, std::string> values;
};
  • 这里编译器不会生成移动构造和移动赋值。
  • 移动请求会转为拷贝操作,性能可能严重下降。

解决方式:显式默认特殊成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class StringTable {
public:
    StringTable() {}

    ~StringTable() { 
        makeLogEntry("Destroying StringTable object");
    }

    StringTable(StringTable&&) = default;            // 显式允许移动构造
    StringTable& operator=(StringTable&&) = default; // 显式允许移动赋值

    StringTable(const StringTable&) = default;       // 显式允许拷贝构造
    StringTable& operator=(const StringTable&) = default; // 显式允许拷贝赋值

private:
    std::map<int, std::string> values;
};
  • 保留了自定义析构功能。
  • 同时允许移动和拷贝操作自动生成,性能恢复。

多态基类示例

1
2
3
4
5
6
7
8
9
10
class Base {
public:
    virtual ~Base() = default;           // 虚析构函数,允许通过基类指针删除派生类对象

    Base(Base&&) = default;              // 支持移动构造
    Base& operator=(Base&&) = default;  // 支持移动赋值

    Base(const Base&) = default;         // 支持拷贝构造
    Base& operator=(const Base&) = default; // 支持拷贝赋值
};

移动与拷贝声明示例

1
2
3
4
5
6
7
8
class Widget {
public:
    Widget(const Widget&) = delete;      // 禁止拷贝构造
    Widget& operator=(const Widget&) = delete; // 禁止拷贝赋值

    Widget(Widget&&) = default;          // 默认移动构造
    Widget& operator=(Widget&&) = default; // 默认移动赋值
};

复杂情况:成员模板不会阻止特殊成员生成

1
2
3
4
5
6
7
8
class Widget {
public:
    template<typename T>
    Widget(const T& rhs);                 // 模板构造函数

    template<typename T>
    Widget& operator=(const T& rhs);     // 模板赋值运算符
};
  • 这些模板版本的构造函数和赋值运算符不会阻止编译器生成默认的 拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符只要其他自动生成的条件都满足
  • 因为模板函数只有在被实例化(即被真正用到)时,才算真正“存在”。
  • 这条规则能防止因为模板存在而意外屏蔽掉重要的默认行为(比如你没写拷贝构造,但因为写了模板而被“误当成已有构造函数”)。这在 C++11 之后是一个明确设计。

总结

  • 特殊成员函数是编译器自动生成的六个成员函数。
  • 移动操作生成规则更严格,声明拷贝或析构函数会阻止生成。
  • 拷贝构造和拷贝赋值是相互独立的。
  • 用户声明析构函数会阻止移动操作生成,可能导致性能下降。
  • 显式用 = default 声明特殊成员函数是良好习惯,表达意图,避免隐式错误。
  • 模板成员函数不会阻止特殊成员函数生成。
本文由作者按照 CC BY 4.0 进行授权