C++ 的六类特殊成员函数处理默认构造、析构、复制与移动。编译器按条件声明它们,再根据基类和成员判断默认实现是否可用;并不是“没有手写,就总会得到六个正常工作的函数”。
本文按 C++17 讨论。原来的 AbstractTemplate() = default; 与 virtual ~AbstractTemplate() = default; 分别要求默认构造实现和虚析构实现,后者仍是一项用户声明,会影响隐式移动函数的生成。
声明了,不等于能用
跳转到“声明了,不等于能用”下表针对普通类,列出决定是否隐式声明的主要条件;某项已经由用户声明时,不会再为同一项隐式补一份。即使满足条件,默认实现仍可能被定义为删除,调用时会报错。
| 特殊成员 | 常见形式 | 隐式声明条件 |
|---|---|---|
| 默认构造 | T() | 没有用户声明的构造函数 |
| 析构 | ~T() | 没有用户声明的析构函数 |
| 拷贝构造 | T(const T&) | 没有用户声明的拷贝构造;已声明移动构造或移动赋值时,隐式拷贝构造被删除 |
| 拷贝赋值 | T& operator=(const T&) | 没有用户声明的拷贝赋值;已声明移动构造或移动赋值时,隐式拷贝赋值被删除 |
| 移动构造 | T(T&&) | 没有用户声明的拷贝构造、拷贝赋值、移动赋值、析构函数 |
| 移动赋值 | T& operator=(T&&) | 没有用户声明的拷贝构造、移动构造、拷贝赋值、析构函数 |
拷贝函数的参数在某些成员只能接收非 const 引用的情况下可能是 T&;表中是最常见形式。相关规则见 默认构造函数、拷贝与移动构造 和 拷贝与移动赋值。
常见的“已声明但被删除”原因包括:成员不可复制、所需基类构造不可访问、引用成员缺少初始化,或默认赋值需要修改 const 标量或重新绑定引用。错误应沿着编译器提示追到具体基类或成员,不能只看最外层函数名。
= default 与空函数体不同
跳转到“= default 与空函数体不同”= default 让编译器按语言规则生成对应操作。默认拷贝会复制基类和成员,默认析构会析构它们;手写一个空的拷贝函数体不会自动复制普通成员中的数据。C++17 的 = default 用于符合要求的特殊成员函数,不能给任意普通方法加上它。显式默认定义
首次声明就 = default 的函数不是“用户提供”的函数;先声明、以后在类外 = default,则属于用户提供。这会影响平凡性等性质。不要把 T() = default 简化成“所有数据成员自动清零”:普通自动对象中的未初始化标量,仍可能没有可读取的确定值。给字段写明确的初值通常更容易维护。
普通析构函数也不会自动变成 virtual;若基类析构已是 virtual,派生析构会继续覆盖它。需要通过基类指针销毁派生对象时,应设计相应的虚析构接口。缺少它导致的非虚多态删除是未定义行为,不能保证“只打印 Base destructor”。详细场景见 抽象类与虚析构。
完整程序一:资源成员、默认删除与移动回退
跳转到“完整程序一:资源成员、默认删除与移动回退”#include <cassert>#include <memory>#include <type_traits>#include <utility>
struct AutomaticOwner { std::unique_ptr<int> value = std::make_unique<int>(42);};
struct DestructorDeclared { std::unique_ptr<int> value = std::make_unique<int>(42); ~DestructorDeclared() = default;};
struct ExplicitOwner { std::unique_ptr<int> value = std::make_unique<int>(42); ExplicitOwner() = default; ~ExplicitOwner() = default; ExplicitOwner(const ExplicitOwner&) = delete; ExplicitOwner& operator=(const ExplicitOwner&) = delete; ExplicitOwner(ExplicitOwner&&) = default; ExplicitOwner& operator=(ExplicitOwner&&) = default;};
struct CopyProbe { inline static int copies = 0; inline static int moves = 0; CopyProbe() = default; CopyProbe(const CopyProbe&) { ++copies; } CopyProbe(CopyProbe&&) noexcept { ++moves; }};
struct CopyFallback { CopyProbe value; CopyFallback() = default; CopyFallback(const CopyFallback&) = default; ~CopyFallback() = default;};
struct NeedsArgument { explicit NeedsArgument(int) {}};struct MissingReference { int& reference; MissingReference() = default;};struct InsideDefault { int value; InsideDefault() = default;};struct OutsideDefault { int value; OutsideDefault();};OutsideDefault::OutsideDefault() = default;
int main() { static_assert(std::is_move_constructible_v<AutomaticOwner>); static_assert(!std::is_copy_constructible_v<AutomaticOwner>); static_assert(!std::is_move_constructible_v<DestructorDeclared>); static_assert(std::is_move_constructible_v<ExplicitOwner>); static_assert(!std::is_copy_constructible_v<ExplicitOwner>); static_assert(!std::is_default_constructible_v<NeedsArgument>); static_assert(!std::is_default_constructible_v<MissingReference>); static_assert(std::is_trivially_default_constructible_v<InsideDefault>); static_assert(!std::is_trivially_default_constructible_v<OutsideDefault>);
AutomaticOwner first; AutomaticOwner second = std::move(first); assert(!first.value && *second.value == 42); ExplicitOwner third; ExplicitOwner fourth; fourth = std::move(third); assert(!third.value && *fourth.value == 42);
static_assert(std::is_move_constructible_v<CopyFallback>); CopyFallback a; CopyFallback b = std::move(a); (void)b; assert(CopyProbe::copies == 1 && CopyProbe::moves == 0);}DestructorDeclared 因析构声明而没有隐式移动构造,拷贝又因 unique_ptr 被删除,所以不能从右值构造。ExplicitOwner 则明确恢复移动并禁止复制。
CopyFallback 的类型特征为 true,却没有移动构造函数:它可以把右值绑定到 const T&,实际调用拷贝构造。std::move 只是把表达式转换为相应的右值引用形式;is_move_constructible 检查能否从该形式构造,不保证选中了名为“移动构造”的特殊成员。构造能力类型特征
完整程序二:准确观察六类操作
跳转到“完整程序二:准确观察六类操作”下面保留原笔记 obj1 到 obj4 的操作顺序,但让复制和移动真正处理一个值。固定容量日志不动态分配;此例的移动只处理整数,因此标成 noexcept。换成可能抛出异常的成员后,应重新判断异常保证。
#include <array>#include <cassert>#include <cstddef>#include <string_view>#include <utility>
std::array<std::string_view, 10> events;std::size_t eventCount = 0;void record(std::string_view event) noexcept { assert(eventCount < events.size()); events[eventCount++] = event;}
class MyClass {public: MyClass() : value_(42) { record("default"); } ~MyClass() { record("destroy"); } MyClass(const MyClass& other) : value_(other.value_) { record("copy"); } MyClass(MyClass&& other) noexcept : value_(std::exchange(other.value_, -1)) { record("move"); } MyClass& operator=(const MyClass& other) { value_ = other.value_; record("copy assign"); return *this; } MyClass& operator=(MyClass&& other) noexcept { if (this != &other) value_ = std::exchange(other.value_, -1); record("move assign"); return *this; } int value() const { return value_; }private: int value_;};
int main() { { MyClass obj1; MyClass obj2 = obj1; MyClass obj3 = std::move(obj1); MyClass obj4; obj4 = obj2; obj4 = std::move(obj3); assert(obj1.value() == -1 && obj3.value() == -1); assert(obj2.value() == 42 && obj4.value() == 42); } const std::array<std::string_view, 10> expected{ "default", "copy", "move", "default", "copy assign", "move assign", "destroy", "destroy", "destroy", "destroy" }; assert(eventCount == expected.size()); assert(events == expected);}原输出多列了一次默认构造。上述顺序中只有 obj1、obj4 默认构造;obj2、obj3 分别从已有对象复制和移动。这里把移出后的整数设为 -1 是本类明确定义的行为,不能推导所有标准库对象移出后都变为空或某个固定值。
实际业务类若只组合 std::string、容器和智能指针,优先利用这些成员已经具备的资源管理能力。只有确需自定义所有权或接口策略时,才成组检查复制、移动和析构;不要为了凑齐六个函数手写空实现。