购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.1 实现默认函数和删除函数

在C++中,类具有特殊成员(构造函数、析构函数和赋值操作符),这些成员可以由编译器默认实现,也可以由开发人员提供。但是,编译器默认实现的规则有点复杂,当然这也可能会导致问题。另外,开发人员有时希望防止对象以特定的方式被复制、移动或构造,方法是利用不同的技巧来实现这些特殊成员函数。C++11标准简化了其中的许多函数,允许以我们即将在下面看到的方式实现默认函数或删除函数。

3.1.1 准备工作

阅读此节内容,需要熟悉以下概念:

❍ 特殊成员函数(默认构造函数、析构函数、复制构造函数、移动构造函数、复制赋值操作符、移动赋值操作符)。

❍ 可复制概念(类具有复制构造函数和复制赋值操作符,使创建副本成为可能)。

❍ 可移动概念(类具有移动构造函数和移动赋值操作符,使移动对象成为可能)。

记住这一点后,我们来学习如何定义默认函数和删除函数。

3.1.2 使用方式

使用以下语法指定函数应该如何处理:

❍ 要设置默认函数,请使用=default代替函数体。只有具有default特性的特殊类成员函数才能使用=default:

❍ 如果要删除函数,请使用=delete代替函数体,它可以删除任何函数,包括非成员函数:

使用上述功能可以实现各种设计目标,例如以下示例:

❍ 要实现不可复制且隐式不可移动的类,请将复制构造函数和复制赋值操作符声明为已删除:

❍ 要实现不可复制但可移动的类,请将复制操作符声明为已删除,并显式地实现移动操作符(且提供所需的任何其他构造函数):

❍ 为了确保函数只能使用特定类型的对象调用,并防止类型提升,请为该函数提供已删除的重载(以下自由函数的示例也可应用于任意类成员函数):

3.1.3 工作原理

类有几个特殊的成员,默认情况下,这些成员可以由编译器实现,它们是默认构造函数、复制构造函数、移动构造函数、复制赋值操作符、移动赋值操作符和析构函数(有关移动语义的讨论,请参阅9.9节内容)。如果没有手动实现它们,那么编译器就会默认实现它们,以便可以创建、移动、复制和销毁类的实例。但是,如果显式地提供一个或多个这类特殊方法,根据以下规则编译器将不会生成其他方法:

❍ 如果存在用户自定义的构造函数,则不会生成默认构造函数。

❍ 如果存在用户自定义的虚析构函数,则不会生成默认析构函数。

❍ 如果存在用户自定义的移动构造函数或移动赋值操作符,则默认不会生成复制构造函数和复制赋值操作符。

❍ 如果存在用户自定义的复制构造函数、移动构造函数、复制赋值操作符、移动赋值操作符或析构函数,则默认不会生成移动构造函数和移动赋值操作符。

❍ 如果存在用户自定义的复制构造函数或析构函数,则默认会生成复制赋值操作符。

❍ 如果存在用户自定义的复制赋值操作符或析构函数,则默认会生成复制构造函数。

请注意,最后两条规则是已弃用的规则,编译器可能不再支持它们。

有时,开发人员需要提供这些特殊成员的空实现或隐藏它们,防止以特定方式创建类的实例,一个典型的示例是使一个不应该被复制的类可复制。这方面的经典方法是提供默认构造函数,并隐藏复制构造函数和复制赋值操作符。虽然这样做有效,但显式定义的默认构造函数可以确保该类不再被认为是普通的,因此不再被认为是POD类型。目前的替代方法是使用delete,如上一小节所示。

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

默认实现有几大好处,包括:

❍ 它比显式的更有效。

❍ 非默认实现,即使是空的,也被认为是非普通的。这会影响类型的语义,从而变得不普通(因此,也就变为非POD类型)。

❍ 不需要用户编写显式的默认实现。例如,如果存在用户自定义的移动构造函数,则编译器默认不提供复制构造函数和复制赋值操作符。但是,用户仍然可以显式使用默认函数,并要求编译器提供它们,这样就不必手动执行了。

当编译器在函数定义中遇到=delete时,它将阻止调用该函数。但是,在重载解析过程中仍然会考虑该函数,并且只有当删除的函数是最佳匹配函数时,编译器才会生成错误。例如,前面示例中run()函数提供的重载只能使用long参数,使用其他任何类型(包括int,它通过自动类型提升被提升为long)的参数调用函数会将已删除的重载确定为最佳匹配函数,因此编译器将生成错误:

注意,之前声明的函数不能被删除,因为=delete定义必须是编译单元中的第一个声明:

对于类的特殊成员函数,经验法则(也称为“五法则”)是这样描述的:如果显式定义任何复制构造函数、移动构造函数、复制赋值操作符、移动赋值操作符或析构函数,则必须显式定义或默认所有这些构造函数。

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

3.1.4 延伸阅读

❍ 阅读3.9节,以了解如何使用std::invoke()和所提供参数调用可调用对象。 htlVZvGRGUBHbx1liVcNpihVX0f4edAmzja1i3UqVbQEou9fTB0xAjQGzx5iIsyY

点击中间区域
呼出菜单
上一章
目录
下一章
×