C++ =default和=delete的用法(非常详细)
在 C++ 中,类具有特殊成员(构造函数、析构函数和赋值操作符),这些成员可以由编译器默认实现,也可以由开发人员提供。
但是,编译器默认实现的规则有点复杂,当然这也可能会导致问题。另外,开发人员有时希望防止对象以特定的方式被复制、移动或构造。为此,C++11标准简化了其中的许多函数,允许以我们即将在下面看到的方式实现默认函数或删除函数。
使用以下语法指定函数应该如何处理:
1) 要设置默认函数,请使用 =default 代替函数体。只有具有 default 特性的特殊类成员函数才能使用 =default:
2) 如果要删除函数,请使用 =delete 代替函数体,它可以删除任何函数,包括非成员函数:
使用上述功能可以实现各种设计目标,例如以下示例:
1) 要实现不可复制且隐式不可移动的类,请将复制构造函数和复制赋值操作符声明为已删除:
2) 要实现不可复制但可移动的类,请将复制操作符声明为已删除,并显式地实现移动操作符(且提供所需的任何其他构造函数):
3) 为了确保函数只能使用特定类型的对象调用,并防止类型提升,请为该函数提供已删除的重载(以下自由函数的示例也可应用于任意类成员函数):
如果没有手动实现它们,那么编译器就会默认实现它们,以便可以创建、移动、复制和销毁类的实例。但是,如果显式地提供一个或多个这类特殊方法,根据以下规则编译器将不会生成其他方法:
有时,开发人员需要提供这些特殊成员的空实现或隐藏它们,防止以特定方式创建类的实例,一个典型的示例是使一个不应该被复制的类可复制。这方面的经典方法是提供默认构造函数,并隐藏复制构造函数和复制赋值操作符。
虽然这样做有效,但显式定义的默认构造函数可以确保该类不再被认为是普通的,因此不再被认为是 POD 类型。目前的替代方法是使用 delete。
当编译器在函数定义中遇到 =default 时,它将提供默认实现,前面提到的特殊成员函数的规则仍然适用。当且仅当函数是内联函数时,函数可以在类的外部声明为 =default:
当编译器在函数定义中遇到 =delete 时,它将阻止调用该函数。但是,在重载解析过程中仍然会考虑该函数,并且只有当删除的函数是最佳匹配函数时,编译器才会生成错误。
例如,前面示例中 run() 函数提供的重载只能使用 long 参数,使用其他任何类型(包括 int,它通过自动类型提升被提升为 long)的参数调用函数会将已删除的重载确定为最佳匹配函数,因此编译器将生成错误:
注意,之前声明的函数不能被删除,因为 =delete 定义必须是编译单元中的第一个声明:
用户自定义的析构函数、复制构造函数和复制赋值操作符是必需的,因为在各种情况下(例如将参数传递给函数)对象都是从副本构造的。如果它们不是用户自定义的,则由编译器提供,但它们的默认实现可能是错误的。如果类管理资源,那么默认实现执行浅拷贝,这意味着它复制资源句柄的值(例如指向对象的指针),而不是资源本身。在这种情况下,用户自定义的实现必须执行深拷贝来复制资源,而不是复制资源句柄。此时,移动构造函数和移动赋值操作符的出现是必要的,因为它们意味着性能的提高。缺少这两者也不算错误,只是会错过优化机会。
但是,编译器默认实现的规则有点复杂,当然这也可能会导致问题。另外,开发人员有时希望防止对象以特定的方式被复制、移动或构造。为此,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;默认实现有几大好处,包括:
- 它比显式的更有效;
- 非默认实现,即使是空的,也被认为是非普通的。这会影响类型的含义,从而变得不普通(因此,也就变为非 POD 类型);
- 不需要用户编写显式的默认实现。例如,如果存在用户自定义的移动构造函数,则编译器默认不提供复制构造函数和复制赋值操作符。但是,用户仍然可以显式使用默认函数,并要求编译器提供它们,这样就不必手动执行了。
当编译器在函数定义中遇到 =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对于类的特殊成员函数,经验法则(也称为“五法则”)是这样描述的:如果显式定义任何复制构造函数、移动构造函数、复制赋值操作符、移动赋值操作符或析构函数,则必须显式定义或默认所有这些构造函数。
用户自定义的析构函数、复制构造函数和复制赋值操作符是必需的,因为在各种情况下(例如将参数传递给函数)对象都是从副本构造的。如果它们不是用户自定义的,则由编译器提供,但它们的默认实现可能是错误的。如果类管理资源,那么默认实现执行浅拷贝,这意味着它复制资源句柄的值(例如指向对象的指针),而不是资源本身。在这种情况下,用户自定义的实现必须执行深拷贝来复制资源,而不是复制资源句柄。此时,移动构造函数和移动赋值操作符的出现是必要的,因为它们意味着性能的提高。缺少这两者也不算错误,只是会错过优化机会。