显式默认设置的函数和已删除的函数

在 C++11 中,默认和已删除的函数可使您显式控制是否自动生成特殊成员函数。 已删除功能还可让您的简单语言防止有问题的类型提升出现在参数传递给所有类型的特殊成员函数,以及功能会导致意外的常规成员函数和非成员函数调用。

显式默认的和删除的功能的优点

在C++中,如果类型没有声明自身函数,则编译器自动为一个类型生成默认值构造函数、复制构造函数、复制赋值运算符和析构函数。 这些函数称为“特殊成员函数”,它们是使 C++ 行为中简单的用户定义的类型与在 C 中执行的结构相似的函数。 即无需额外编码工作就可创建、复制和销毁它们。 C++11 移动构造函数和移动赋值运算符添加到编译器可以自动生成的特殊成员函数的列表中,就能让语言具有移动语义。

对于简单的类型很方便,但对于复杂类型,通常要自定义一个或多个特殊成员函数,因此这可以阻止自动生成其他特殊成员函数。 实际上:

  • 如果对任何构造函数进行了显式声明(包括对基类型),则不会自动生成默认构造函数。

  • 如果对任何析构函数进行了显式声明(包括对基类型),则不会自动生成默认析构函数。

  • 如果对移动构造函数或移动赋值运算符进行显式声明,则:

    • 复制构造函数不会自动生成。

    • 复制赋值运算符不会自动生成。

  • 如果复制构造函数、复制赋值运算符、移动构造函数、移动分配运算符或析构函数显式声明,则:

    • 移动构造函数不会自动生成。

    • 移动赋值运算符不会自动生成。

备注

另外,该 C++11 标准指定以下附加规则:

  • 如果复制构造函数或析构函数显式声明,则自动一代复制赋值运算符已弃用。

  • 如果复制赋值运算符或析构函数显式声明,则自动一代复制赋值构造函数已弃用。

在这两种情况下,Visual Studio 继续隐式自动生成所需的功能和不发出警告。

这些规则的结果也可能泄漏到对象的层次结构。 例如,如果因为任何原因一个基类不能有一个默认的可从派生类调用的构造函数—也就是说,一个public 或protected 无参数构造函数—然后派生于它的类自动生成自己的默认构造函数

这些规则可以将本是相对简单直接的内容实现、用户定义的类型以及通用 C++ 惯例复杂化,例如通过声明复制构造函数和专有赋值运算符复制而不对其进行定义,使用户定义的类型变得不可复制。

struct noncopyable
{
  noncopyable() {};
  
private:
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

在 C++11 之前,此代码片段是不可复制类型的惯用窗体。 但是,其中有若干问题:

  • 复制构造函数必须私下声明以便隐藏,但由于它已完全声明,避免了默认值构造函数的自动生成。 如果您需要一个,则您必须显式定义默认构造函数,即使其不执行任何操作,也是如此。

  • 即使显式定义的默认构造函数不执行任何操作,编译器也会认为它很重要。 它比一个自动生成的默认构造函数效率低并防止noncopyable 真正的 POD 类型变得不可复制。

  • 尽管复制构造函数和复制赋值运算符对外部代码是隐藏的,成员函数和noncopyable 友元依然可以看见并调用它们。 如果已经声明但没有定义,则对其进行调用时将导致链接器错误。

  • 虽然这是一个通常接受的惯例,但除非您理解针对特殊成员函数的自动生成的所有规则,否则该目的是不会清晰的。

在 C++11 中,non-copyable 习语可通过更加直接的方法实现。

struct noncopyable
{
  noncopyable() =default;
  noncopyable(const noncopyable&) =delete;
  noncopyable& operator=(const noncopyable&) =delete;
};

注意如何解决带有预 C++11 习惯用语的问题:

  • 通过声明复制构造函数仍将阻止默认构造函数的生成,但是可以通过将其显式默认而进行恢复。

  • 仍将显式默认的特殊成员函数视为非重要的,因此未导致性能损失,并且不妨碍 noncopyable 成为真正的 POD 类型。

  • 复制构造函数和复制赋值运算符是公共的,但已删除。 它是定义或调用某个已删除功能的编译时错误。

  • 对于了解=default and =delete的人而言,此举的目的非常清楚。 您不必了解特殊成员函数的自动生成的规则。

类似的惯例存在于不可移动、只能动态分配或不能动态分配的用户定义类型的创建过程中。 这些惯例中的每一个都有具有类似问题的预 C++11 实现,该实现已在 C++11 通过实现它们的默认和删除的特定成员函数得到类似解决。

显式默认的功能

您可默认任何特殊成员函数以指出特殊成员函数使用默认实现,使用非公共访问限定符定义特殊成员函数,或恢复自动生成被其他条件阻止的特殊成员函数。

您通过声明来默认特殊成员函数,如以下示例所示:

struct widget
{
  widget()=default;

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

inline widget& widget::operator=(const widget&) =default;

注意,您可以在类体的外部默认特定的成员函数,只要它是可以内联的。

因为微小的特殊成员函数具有性能上的优点,所以推荐您在需要默认行为时使用自动生成的特殊成员函数,而不是空函数体。 您可通过显式默认特殊成员函数或通过对其进行声明(并不声明会阻止其自动生成的其他特殊成员函数)来执行此操作。

备注

Visual Studio 不支持默认的移动构造函数或移动赋值运算符作为c++ 11标准要求。有关详情,请参阅 对 C++11 功能的支持(现代 C++) 的默认和已删除的功能一节。

已删除的函数

您可删除特殊成员函数以及常规成员函数和非成员函数以避免其被定义或调用。 删除特殊成员函数提供了一种更加干净利落的方式去阻止编译器生成您不想要的特定成员函数。 函数声明必须删除;它不可以事后以一个函数被声明然后作为默认函数的方式被删除

struct widget
{
  // deleted operator new prevents widget from being dynamically allocated.
  void* operator new(std::size_t) =delete;
};

删除常规成员函数或非成员函数能阻止有问题的类型提升导致意外的函数调用。 此方法很有效,因为已删除的函数仍然可参与重载决策,并且比提升类型后调用函数提供的匹配度还要好。 函数调用解析到更具体的,但可删除的功能,并导致编译器错误。

// deleted overload prevents call through type promotion of float to double from succeeding.
void call_with_true_double_only(float) =delete;
void call_with_true_double_only(double param) { return; }

在前面的示例中通过float参数调用call_with_true_double_only 将会引起一个编译错误, 但是调用 call_with_true_double_only 通过一个 int 参数将不会; 在int 情况下, 参数将会从 int提升到double 并且 成功调用 函数的double 版本, 即使那可能不是其本意 若要确保任何对此函数的调用使用非二进制文件参数导致编译器错误,可以声明删除功能的模板版本。

template < typename T >
void call_with_true_double_only(T) =delete; //prevent call through type promotion of any T to double from succeeding.

void call_with_true_double_only(double param) { return; } // also define for const double, double&, etc. as needed.