Effective Modern C++ 条款 17 编译器自动生成/合成的特种成员函数总结

本文承接上文,总结自《Effective Modern C++》条款 17:理解特种成员函数的生成机制

特种成员函数

  • C++98 四种:默认构造、析构、复制构造、复制赋值运算符
  • C++11增加两种:移动构造、移动赋值运算符

合成机制

  • 只有用到了才合成

  • 没有显式声明任何构造的时候才会合成默认构造

  • 合成的都是 public、inline 的

  • 合成的都是非虚函数,除非该类的基类析构是虚函数

  • C++11 的析构默认是 noexcept 的

  • 两种复制操作彼此独立,声明一个,不影响另一个(为兼容遗留代码,目前生成声明一个复制操作,依然会合成另一个,但最好用 =default 显式声明,因为后续标准可能会改变该行为)

  • 两种移动操作彼此关联,声明一个,不会自动合成另一个

  • 一旦声明了复制操作,就不会合成移动操作;反之亦然,一旦声明移动操作,就废除复制操作

  • Rule of Three 指导原则:如果声明了复制构造、复制赋值、析构中的任何一个,就得同时声明所有 3 个

    • 推论:如果声明了析构,则不该合成复制操作(为兼容遗留代码,不影响合成复制操作,但已废弃,后续标准可能改变)
    • C++11 规定只要声明了析构,就不会合成移动
  • 合成的移动操作实际上是“按成员移动”请求。如果成员不支持移动操作,则执行复制操作

  • 建议:使用 = default 清晰表达意图,避免错误

    • 例如原本仅声明默认构造的程序,新增加一个析构(打印 log),将导致禁用合成移动操作,功能测试仍可通过,但是本可以执行移动操作的现在只能复制,慢若干数量级
  • 成员函数模版在任何情况下都不抑制特种成员函数合成(P111)

总结

  • 只有用到了才合成,默认都是 public、inline、非虚(特例:如果父类析构为虚,合成的子类析构为虚)
  • 默认构造:类中不含用户声明的构造时才合成
  • 析构:基类析构为虚才为虚,C++11 中默认为 noexcept
  • 复制构造:按成员进行非静态成员复制构造。如果声明了移动操作,则删除复制构造。已有复制赋值或析构,仍然可以合成复制构造,但标记为 deprecated
  • 复制赋值:按成员进行非静态成员的复制赋值。如果声明了移动操作,则删除复制赋值。已有复制构造或析构,仍然可以合成复制赋值,但标记为 deprecated
  • 移动构造和移动赋值:按成员进行非静态成员移动。限制条件多,仅在类中不含显式复制、移动、析构声明时才合成,声明一个移动操作也会抑制另一个。移动操作也会移动其基类部分

测试代码

#include <iostream>
using namespace std;

class Foo {
 public:
  Foo() = default;
  Foo(Foo&& f) { cout << "moving Foo" << endl; };
  Foo(const Foo& f) { cout << "copying Foo" << endl; };
};

class Bar {
 public:
//   virtual ~Bar() = default;
//   Bar& operator=(const Bar& b) = delete;

 private:
  Foo f;
};

int main() {
  Bar a;
  Bar b(move(a));
}

直接运行上述代码输出 moving Foo,打开任意一个注释,输出 copying Foo。说明 =delete=default 都会被编译器认为显式声明。

扩展阅读

posted @ 2022-04-30 11:13  Zijian/TENG  阅读(113)  评论(0编辑  收藏  举报