C++defaulted与deleted函数
defaulted 和 deleted:C++里那两个“不干活却很忙”的函数
写C++时,你有没有过这种时刻:类里啥都没写,编译器却悄悄给你塞了拷贝构造、赋值操作——结果某天对象被意外复制,程序在深夜core dump,而你翻了半小时代码才意识到,问题出在那个你根本没声明、更没定义的默认拷贝函数上?
这正是 defaulted 和 deleted 出场的理由。它们不是语法糖,也不是炫技工具;它们是C++11给程序员配发的“函数控制权移交协议”——让你明确告诉编译器:“这个函数,我接管了;它该干啥,或不该干啥,我说了算。”
默认函数,从来就不“默认”
很多人误以为 = default 是“让编译器生成默认实现”,其实更准确的理解是:它显式启用编译器自动生成的特殊成员函数,且仅当该函数本就符合隐式生成条件时才合法。
比如:
struct Widget {
std::string name;
int id = 0;
Widget() = default; // ✅ 合法:无用户自定义构造,编译器本就会生成
Widget(const Widget&) = default; // ✅ 同理
};
但如果你加了自定义析构函数:
struct BadWidget {
~BadWidget() { /* 自定义清理 */ }
Widget(const BadWidget&) = default; // ❌ 编译错误!
};
为什么?因为一旦你声明了析构函数(哪怕只是空实现),编译器就不再自动生成拷贝构造和拷贝赋值——这是C++98遗留的保守策略。= default 并不能“绕过规则”,它只是“申请启用”,而编译器会严格审核:你申请的函数,在当前类定义下是否仍满足隐式生成的前提条件? 不满足,直接报错。这不是bug,是安全阀。
= delete:不是禁用,而是“精准封印”
delete 最常被当作“禁止拷贝”的快捷键,比如:
class NonCopyable {
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
public:
NonCopyable() = default;
};
但它的真正价值,远不止于此。delete 的核心能力,是参与重载决议,并在匹配阶段主动失败——这意味着它能干掉“危险但合法”的隐式转换。
举个真实场景:你写了一个接受 int 的构造函数,想支持 Widget w(42);,但又不希望 Widget w = 42; 这种隐式转换发生(毕竟后者会触发拷贝初始化,可能绕过你的资源管理逻辑):
class Widget {
public:
explicit Widget(int x) : val(x) {}
// 错误示范:只加 explicit,仍允许 Widget w = 42;(编译器会尝试隐式转换+拷贝)
// 正确做法:
Widget(double) = delete; // 封印所有浮点数隐式转换
Widget(const char*) = delete; // 封印字符串字面量误用
private:
int val;
};
这里 = delete 不是“补漏”,而是主动设计接口边界:它让 Widget w = 3.14; 在编译期就失败,错误信息清晰指向“调用了已删除的函数”,比运行时断言或静默截断强得多。
组合拳:defaulted + deleted 解决真实矛盾
最典型的冲突场景:移动语义引入后,拷贝与移动的共存规则变得微妙。假设你有一个管理动态内存的类:
class Buffer {
char* data_;
size_t size_;
public:
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
// 问题来了:拷贝构造要不要?如果删掉,用户无法拷贝;
// 如果留着默认,编译器不会生成(因移动构造存在),但你也没写——结果连拷贝都不可用。
Buffer(const Buffer&) = delete; // 明确拒绝拷贝
Buffer& operator=(const Buffer&) = delete;
// 但移动赋值呢?必须显式定义,否则默认生成的可能不安全
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
};
注意:移动赋值不能 = default ——因为 = default 会生成逐成员移动,而 char* 的移动本质仍是浅拷贝,析构时双重释放风险极高。这里必须手写,= delete 则用于彻底关闭你不信任的操作路径。
写在最后:它们不是修饰符,是契约
defaulted 和 deleted 看似轻巧,实则是C++对“意图明确性”的一次郑重升级。
它们逼你直面一个问题:这个函数,是应该存在、不存在,还是以某种特定方式存在?
不写任何声明,靠编译器猜;写了 = default,是你确认“这个默认行为对我安全”;写了 = delete,是你宣告“这个行为在此上下文中没有意义,甚至有害”。
下次当你犹豫要不要删掉某个拷贝函数,或者纠结移动操作该不该手写时,别急着查文档——先问自己一句:我此刻,是在让编译器干活,还是在和它签一份责任契约?


还没有评论,来说两句吧...