=default 和 =delete
=default
=default 是 C++11 引入的一个关键字。它用于显式地要求编译器为类的特殊成员函数生成默认的实现。
特殊成员函数是指那些在某些情况下编译器会自动为生成的函数,主要包括:
- 默认构造函数 (Default Constructor): ClassName()
- 析构函数 (Destructor): ~ClassName()
- 拷贝构造函数 (Copy Constructor): ClassName(const ClassName&)
- 拷贝赋值运算符 (Copy Assignment Operator): ClassName& operator=(const ClassName&)
- 移动构造函数 (Move Constructor) (C++11 起): ClassName(ClassName&&)
- 移动赋值运算符 (Move Assignment Operator) (C++11 起): ClassName& operator=(ClassName&&)
使用 default 的语法是在类的定义中,在特殊成员函数的声明后面加上 = default;
class ClassName {
public:
// 显式要求编译器生成默认构造函数
ClassName() = default;
// 显式要求编译器生成析构函数
~ClassName() = default;
// 显式要求编译器生成拷贝构造函数
ClassName(const ClassName&) = default;
// 显式要求编译器生成拷贝赋值运算符
ClassName& operator=(const ClassName&) = default;
// 显式要求编译器生成移动构造函数
ClassName(ClassName&&) = default;
// 显式要求编译器生成移动赋值运算符
ClassName& operator=(ClassName&&) = default;
private:
int data;
std::string name;
};
引入 default 关键字支持编译器的合成行为,当明确要求编译器生成某个特殊成员函数时,可以使用关键字 =default;
=default 的作用是恢复或显示要求编译器生成特殊成员函数的默认实现。
=delete
= delete 是 C++11 引入的一个新特性,它用于显式地禁用某个函数(通常是类的特殊成员函数,但也可以是普通成员函数或非成员函数)。当一个函数被声明为 = delete 后,任何试图调用该函数的代码都会在编译时产生错误。
= delete 与 = default 有相同的应用场景,主要应用于特殊成员函数:
- 默认构造函数
- 拷贝构造函数
- 拷贝赋值函数
- 移动构造函数
- 移动赋值函数
- 析构函数
为什么引用 =delete 关键字?
对于某些类,执行拷贝或者移动操作可能并没有意义,或者可能导致危险的行为。例如,管理独占的资源(如文件句柄、网络套接字、互斥锁std::mutex、智能指针)的类。默认生成的拷贝构造函数和拷贝赋值函数通常执行"浅拷贝",这可能导致多个对象指向同一资源,从而引发资源重复释放、悬挂指针等问题。通过将拷贝构造函数和拷贝赋值函数标记为 =delete,可以从根本上阻止对象的拷贝操作。
或阻止不期望的隐式类型转换,比如,构造函数可以定义从其他类型的类类型的隐式转换。某些转换可能是不希望发生的或者有歧义的。通过将接受特定类型的构造函数标记为 = delete,可以阻止这种特定的隐式转换。
例如,你可能希望你的类可以从 int 构造,但明确禁止从 bool 或 double 构造,即使它们可以隐式转换为 int。
class ClassName {
public:
ClassName(int i) : value(i) {}
ClassName(bool) = delete; // 禁止从 bool 构造
ClassName(double) = delete; // 禁止从 double 构造
private:
int value;
};
ClassName n1(10); // OK
// ClassName n2(true); // 编译错误
// ClassName n3(3.14); // 编译错误
= delete 本身是一个编译器时特性,它直接在编译期决定代码的行为,而非运行时的内存操作。
现代 C++ 开发中的 =delete 和 =default
=default 显示要求编译器生成默认实现
实践案例
#include <iostream>
#include <string>
#include <vector>
class MyWidget {
private:
int id_;
std::string name_;
std::vector<double> data_;
public:
// 显式要求默认构造函数 (即使我们定义了其他构造函数)
MyWidget() = default;
// 自定义构造函数
MyWidget(int id, std::string name) : id_(id), name_(std::move(name)) {}
// 显式要求编译器生成拷贝构造函数
// 如果没有这行,由于定义了析构函数(见下),编译器可能不会生成
MyWidget(const MyWidget&) = default;
// 显式要求编译器生成拷贝赋值运算符
MyWidget& operator=(const MyWidget&) = default;
// 显式要求编译器生成移动构造函数
MyWidget(MyWidget&&) noexcept = default;
// 显式要求编译器生成移动赋值运算符
MyWidget& operator=(MyWidget&&) noexcept = default;
// 显式要求编译器生成默认析构函数
// (即使它默认就是 virtual 的,如果基类是 virtual)
// 或者只是为了明确表达使用默认析构行为
virtual ~MyWidget() = default; // 如果打算作为基类,析构函数通常需要是 virtual
void print() const {
std::cout << "Widget ID: " << id_ << ", Name: " << name_ << std::endl;
}
};
// 另一个例子: 强制生成默认构造函数
class Controller {
public:
// 如果没有 = default,因为定义了带参数的构造函数,
// 编译器就不会生成默认构造函数了。
Controller() = default;
Controller(int /*some_config*/) { /* ... */ }
};
=delete 显示禁止编译器生成或者使用某个函数
实践案例
#include <mutex> // std::mutex 就是不可拷贝的
// 1. 禁止拷贝和移动 (例如,一个 RAII 资源管理器)
class UniqueResource {
private:
int* resource_ptr_;
public:
explicit UniqueResource(int val) : resource_ptr_(new int(val)) {}
~UniqueResource() { delete resource_ptr_; }
// 禁止拷贝构造
UniqueResource(const UniqueResource&) = delete;
// 禁止拷贝赋值
UniqueResource& operator=(const UniqueResource&) = delete;
// 可以选择允许移动 (如果需要的话)
UniqueResource(UniqueResource&& other) noexcept : resource_ptr_(other.resource_ptr_) {
other.resource_ptr_ = nullptr;
}
UniqueResource& operator=(UniqueResource&& other) noexcept {
if (this != &other) {
delete resource_ptr_;
resource_ptr_ = other.resource_ptr_;
other.resource_ptr_ = nullptr;
}
return *this;
}
// 或者也禁止移动
// UniqueResource(UniqueResource&&) = delete;
// UniqueResource& operator=(UniqueResource&&) = delete;
};
// 2. 禁止不期望的构造/转换
class IntegerOnly {
public:
IntegerOnly(int i) : value_(i) {}
// 禁止从 bool, char, double 等构造,防止意外转换
IntegerOnly(bool) = delete;
IntegerOnly(char) = delete;
IntegerOnly(double) = delete;
// ... 可以根据需要删除其他类型
// (注意:C++11 起可以用 explicit 构造函数部分达到此目的,
// 但 =delete 更强力,完全禁止)
private:
int value_;
};
// 3. 禁止堆分配
class StackOnly {
public:
StackOnly() = default;
private:
// 禁止 operator new
void* operator new(std::size_t) = delete;
void* operator new[](std::size_t) = delete;
// (注意:还需要考虑 delete,但禁止 new 通常是主要目的)
};
使用开发规范与注意事项
-
遵循零原则(Rule of Zero):优先让编译器自动生成特殊成员函数。只有当默认行为不符合要求时,才考虑自定义实现。
-
明确优先性:当需要依赖某个特殊成员的默认行为、但又因为定义了其他特殊成员函数导致其可能不被生成时,使用 =default 来明确指定需要的默认实现。
-
成对使用 =delete:如果决定禁止拷贝,通常应该同时 delete 拷贝构造函数和拷贝赋值函数,同理适用于移动操作。
-
=delete 提供更清晰的错误: 相比于旧的将拷贝构造/赋值声明为 private 且不实现的技巧,=delete 能在编译阶段产生更清晰、更直接的错误信息。
-
=delete 的广泛性: =delete 可以用于任何函数签名,包括普通成员函数、非成员函数和模板特化。
-
继承的影响: 如果基类的某个特殊成员函数被 =delete 或无法访问(private),那么派生类对应的特殊成员函数通常也会被隐式地 =delete。如果基类的析构函数被 =delete 或无法访问,则无法创建派生类对象。