C++ =default 和 =delete

=default 和 =delete

=default

=default 是 C++11 引入的一个关键字。它用于显式地要求编译器为类的特殊成员函数生成默认的实现。

特殊成员函数是指那些在某些情况下编译器会自动为生成的函数,主要包括:

  1. 默认构造函数 (Default Constructor): ClassName()
  2. 析构函数 (Destructor): ~ClassName()
  3. 拷贝构造函数 (Copy Constructor): ClassName(const ClassName&)
  4. 拷贝赋值运算符 (Copy Assignment Operator): ClassName& operator=(const ClassName&)
  5. 移动构造函数 (Move Constructor) (C++11 起): ClassName(ClassName&&)
  6. 移动赋值运算符 (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 有相同的应用场景,主要应用于特殊成员函数:

  1. 默认构造函数
  2. 拷贝构造函数
  3. 拷贝赋值函数
  4. 移动构造函数
  5. 移动赋值函数
  6. 析构函数

为什么引用 =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 通常是主要目的)
};

使用开发规范与注意事项

  1. 遵循零原则(Rule of Zero):优先让编译器自动生成特殊成员函数。只有当默认行为不符合要求时,才考虑自定义实现。

  2. 明确优先性:当需要依赖某个特殊成员的默认行为、但又因为定义了其他特殊成员函数导致其可能不被生成时,使用 =default 来明确指定需要的默认实现。

  3. 成对使用 =delete:如果决定禁止拷贝,通常应该同时 delete 拷贝构造函数和拷贝赋值函数,同理适用于移动操作。

  4. =delete 提供更清晰的错误: 相比于旧的将拷贝构造/赋值声明为 private 且不实现的技巧,=delete 能在编译阶段产生更清晰、更直接的错误信息。

  5. =delete 的广泛性: =delete 可以用于任何函数签名,包括普通成员函数、非成员函数和模板特化。

  6. 继承的影响: 如果基类的某个特殊成员函数被 =delete 或无法访问(private),那么派生类对应的特殊成员函数通常也会被隐式地 =delete。如果基类的析构函数被 =delete 或无法访问,则无法创建派生类对象。