跳转到内容
新建笔记

C++ 特殊成员函数:默认化、删除、复制与移动

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、容器和智能指针,优先利用这些成员已经具备的资源管理能力。只有确需自定义所有权或接口策略时,才成组检查复制、移动和析构;不要为了凑齐六个函数手写空实现。