首页 > 编程笔记 > C++笔记 阅读:2

C++ =default和=delete的用法(非常详细)

C++ 中,类具有特殊成员(构造函数、析构函数和赋值操作符),这些成员可以由编译器默认实现,也可以由开发人员提供。

但是,编译器默认实现的规则有点复杂,当然这也可能会导致问题。另外,开发人员有时希望防止对象以特定的方式被复制、移动或构造。为此,C++11标准简化了其中的许多函数,允许以我们即将在下面看到的方式实现默认函数或删除函数。

使用以下语法指定函数应该如何处理:
1) 要设置默认函数,请使用 =default 代替函数体。只有具有 default 特性的特殊类成员函数才能使用 =default:
struct foo
{
    foo() = default;
};

2) 如果要删除函数,请使用 =delete 代替函数体,它可以删除任何函数,包括非成员函数:
struct foo
{
    foo(foo const &) = delete;
};

void func(int) = delete;

使用上述功能可以实现各种设计目标,例如以下示例:
1) 要实现不可复制且隐式不可移动的类,请将复制构造函数和复制赋值操作符声明为已删除:
class foo_not_copyable
{
public:
    foo_not_copyable() = default;

    foo_not_copyable(foo_not_copyable const &) = delete;
    foo_not_copyable& operator=(foo_not_copyable const&) = delete;
};

2) 要实现不可复制但可移动的类,请将复制操作符声明为已删除,并显式地实现移动操作符(且提供所需的任何其他构造函数):
class data_wrapper
{
    Data* data;
public:
    data_wrapper(Data* d = nullptr) : data(d) {}
    ~data_wrapper() { delete data; }

    data_wrapper(data_wrapper const&) = delete;
    data_wrapper& operator=(data_wrapper const &) = delete;

    data_wrapper(data_wrapper&&& other) :data(std::move(other.data))
    {
        other.data = nullptr;
    }

    data_wrapper& operator=(data_wrapper&&& other)
    {
        if (this != std::addressof(other))
        {
            delete data;
            data = std::move(other.data);
            other.data = nullptr;
        }
        return *this;
    }
};

3) 为了确保函数只能使用特定类型的对象调用,并防止类型提升,请为该函数提供已删除的重载(以下自由函数的示例也可应用于任意类成员函数):
template <typename T>
void run(T val) = delete;

void run(long val) {} // can only be called with Long integers

C++ =default和=delete的工作原理

C++ 的类有几个特殊的成员,默认情况下,这些成员可以由编译器实现,它们是默认构造函数、复制构造函数、移动构造函数、复制赋值操作符、移动赋值操作符和析构函数。

如果没有手动实现它们,那么编译器就会默认实现它们,以便可以创建、移动、复制和销毁类的实例。但是,如果显式地提供一个或多个这类特殊方法,根据以下规则编译器将不会生成其他方法:
有时,开发人员需要提供这些特殊成员的空实现或隐藏它们,防止以特定方式创建类的实例,一个典型的示例是使一个不应该被复制的类可复制。这方面的经典方法是提供默认构造函数,并隐藏复制构造函数和复制赋值操作符。

虽然这样做有效,但显式定义的默认构造函数可以确保该类不再被认为是普通的,因此不再被认为是 POD 类型。目前的替代方法是使用 delete。

当编译器在函数定义中遇到 =default 时,它将提供默认实现,前面提到的特殊成员函数的规则仍然适用。当且仅当函数是内联函数时,函数可以在类的外部声明为 =default:
class foo
{
public:
    foo() = default;

    inline foo& operator=(foo const &);
};

inline foo& foo::operator=(foo const &) = default;
默认实现有几大好处,包括:
当编译器在函数定义中遇到 =delete 时,它将阻止调用该函数。但是,在重载解析过程中仍然会考虑该函数,并且只有当删除的函数是最佳匹配函数时,编译器才会生成错误。

例如,前面示例中 run() 函数提供的重载只能使用 long 参数,使用其他任何类型(包括 int,它通过自动类型提升被提升为 long)的参数调用函数会将已删除的重载确定为最佳匹配函数,因此编译器将生成错误:
run(42); // error, matches a deleted overload
run(42L); // OK, Long integer arguments are allowed

注意,之前声明的函数不能被删除,因为 =delete 定义必须是编译单元中的第一个声明:
void forward_declared_function();
//...
void forward_declared_function() = delete; // error
对于类的特殊成员函数,经验法则(也称为“五法则”)是这样描述的:如果显式定义任何复制构造函数、移动构造函数、复制赋值操作符、移动赋值操作符或析构函数,则必须显式定义或默认所有这些构造函数。

用户自定义的析构函数、复制构造函数和复制赋值操作符是必需的,因为在各种情况下(例如将参数传递给函数)对象都是从副本构造的。如果它们不是用户自定义的,则由编译器提供,但它们的默认实现可能是错误的。如果类管理资源,那么默认实现执行浅拷贝,这意味着它复制资源句柄的值(例如指向对象的指针),而不是资源本身。在这种情况下,用户自定义的实现必须执行深拷贝来复制资源,而不是复制资源句柄。此时,移动构造函数和移动赋值操作符的出现是必要的,因为它们意味着性能的提高。缺少这两者也不算错误,只是会错过优化机会。

相关文章