本周小贴士#143:C++11 删除的函数(= delete)

作为TotW#143最初发表于2018年3月2日

由Leonard Mosescu创作

介绍

一般来说,接口通常定义了可以调用的操作集。 然而,有时我们可能想要表达相反的意思:明确定义一组不应使用的操作。 例如,禁用拷贝构造函数和拷贝赋值运算符是限制特定类型的拷贝语义的常用方法。

该语言提供了多种选项来影响这些限制(我们将很快探讨每一个):

  1. 提供一个仅由运行时检查组成的虚拟定义。
  2. 使用可访问性控件(受保护/私有)使功能不可访问。
  3. 声明函数,但有意省略定义。
  4. 从 C++11 开始:将函数显式定义为“已删除”。

C++11 之前的技术范围从运行时检查 (#1) 到编译时 (#2) 或链接时 (#3) 诊断。 虽然经过实战验证,但这些技术远非完美:运行时检查对于约束是静态的大多数情况并不理想,并且链接时检查将诊断延迟到构建过程的很晚。 此外,不能保证链接时间诊断(缺少 ODR(一次定义规则) 使用函数的定义是违反 ODR 的)并且实际的诊断消息很少对开发人员友好。

编译时检查更好,但仍有缺陷。 它仅适用于成员函数,并且基于可访问性约束,这些约束很冗长、容易出错并且容易出现漏洞。 此外,引用此类函数导致的错误可能会产生误导,因为它们指的是访问限制而不是接口滥用。

#2 和 #3 禁用复制的应用程序如下所示:

class MyType {
 private:
  MyType(const MyType&);  // 任何地方都没有定义
  MyType& operator=(const MyType&);  // 任何地方都没有定义
  // ...
};

为每个类手动应用它会很快过时,因此开发人员通常以下列方式之一包装它们:

“mixin”方法(boost::noncopyable, non-copyable mixin)

class MyType : private NoCopySemantics {
  ...
};

宏方法

class MyType {
 private:
  DISALLOW_COPY_AND_ASSIGN(MyType);
};

C++11删除的函数

C++11 通过新的语言特性解决了针对更好解决方案的需求:删除定义 [dcl.fct.def.delete]。 (请参阅 C++ 标准草案中的“已删除定义”。)任何函数都可以显式定义为已删除:

void foo() = delete;

语法很简单,类似于默认函数,但有几个显着差异:

  1. 可以删除任何函数,包括非成员函数(与 =default 相比,它仅适用于特殊成员函数)。
  2. 必须仅在第一个声明时删除函数(与 =default 不同)。

要记住的关键是 =delete 是一个函数定义(它不会删除或隐藏声明)。 删除的函数因此被定义并像任何其他函数一样参与名称查找和重载解析。 这是一种特殊的“放射性”定义,上面写着“请勿触摸!”。

尝试使用已删除的函数会导致编译时错误并具有明确的诊断,这是与 C++11 之前的技术相比的主要优势之一。

class MyType {
 public:
  // 使无效默认构造函数
  MyType() = delete;

  // 使无效拷贝(和移动)语义
  MyType(const MyType&) = delete;
  MyType& operator=(const MyType&) = delete;

  //...
};
// 错误:调用删除的“MyType”构造函数
// 注意:‘MyType’在此处已经显式标记为删除的
// MyType() = delete;
MyType x;

void foo(const MyType& val) {
  // 错误:调用删除的“MyType”构造函数
  // 注意:‘MyType’在此处已经显式标记为删除的
  // MyType(const MyType&) = delete;
  MyType copy = val;
}

注意:通过将复制操作显式定义为已删除,我们还抑制了移动操作(具有用户声明的复制操作会抑制移动操作的隐式声明)。 如果打算使用隐式移动操作定义仅移动类型,则 =default 可用于“将它们带回来”,例如:

MyType(MyType&&) = default;
MyType& operator=(MyType&&) = default;

其他用途

虽然上面的示例以拷贝语义为中心(这可能是最常见的情况),但任何函数(无论是否成员)都可以删除。

由于已删除的函数参与重载解析,它们可以帮助捕获意外使用。 假设我们有以下重载的print函数:

void print(int value);
void print(absl::string_view str);

当开发人员可能打算使用 print(“x”) 时,调用 print(‘x’) 将打印“x”的整数值。 我们可以获得这个:

void print(int value);
void print(const char* str);
// 使用字符串字面量":"代码字符字面量':'
void print(char) = delete;

请注意,=delete 不仅仅影响函数调用。 尝试获取已删除函数的地址也会导致编译错误:

void (*pfn1)(int) = &print;  // 正确
void (*pfn2)(char) = &print; // 错误:尝试使用删除了的函数

这个例子是从一个真实世界的应用程序中提取的:absl::StrCat()。 每当必须限制界面的特定部分时,删除函数都很有用。

将析构函数定义为已删除比将它们设为私有更严格(尽管这是一个大锤子,它可能会引入比预期更多的限制)

// 一个严格的限制类型
// 1. 仅仅动态存储。
// 2. 永远存在(不会被析构)
// 3. 不能作为成员或基类
class ImmortalHeap {
 public:
  ~ImmortalHeap() = delete;
  //...
};

还有一个例子,这次我们只想允许分配非数组对象([real world example][crashpad]):

// 不允许new T[].
class NoHeapArraysPlease {
 public:
  void* operator new[](std::size_t) = delete;
  void operator delete[](void*) = delete;
};

auto p = new NoHeapArraysPlease;  // 正确

// 错误:调用删除了的函数'operator new[]'
// 注意:假造函数已经被删除
// void* operator new[](std::size_t) = delete;
auto pa = new NoHeapArraysPlease[10];

总结

=delete 提供了一种明确的方式来表达不应被引用的接口部分,还可以比 C++11 之前的习语提供更好的诊断。 任何代码,包括编译器生成的代码,都不能引用已删除的函数。 对于细致入微的访问控制,访问说明符或更复杂的技术(例如,提示 #134 中讨论的密码习语)更合适。

重要提示:由于已删除的定义是接口的一部分,因此它们应该与接口的其他部分具有相同的访问说明符。 具体来说,这意味着它们通常应该是公开的。 在实践中,这也会产生最好的诊断(private 和 =delete 没有多大意义)。

致谢:此提示包括许多人的关键贡献和反馈,特别感谢:Mark Mentovai、James Dennett、Bruce Dawson 和 Yitzhak Mandelbaum。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值