析构函数中的怪异枚举


83

目前,我正在阅读的源代码Protocol Buffer,并且在这里找到了一个奇怪的enum代码

  ~scoped_ptr() {
    enum { type_must_be_complete = sizeof(C) };
    delete ptr_;
  }

  void reset(C* p = NULL) {
    if (p != ptr_) {
      enum { type_must_be_complete = sizeof(C) };
      delete ptr_;
      ptr_ = p;
    }
  }

为什么在enum { type_must_be_complete = sizeof(C) };这里定义?这有什么用途?


2
如果我想是肯定的,那么我宁愿用ptr_自己的sizeof作为sizeof(*ptr_),而不是sizeof(C)
纳瓦兹

Answers:


81

此技巧通过在编译此析构函数时确保C的定义可用来避免UB。否则,编译将失败,因为无法确定sizeof不完整的类型(正向声明的类型),但可以使用指针。

在编译的二进制文件中,此代码将被优化,并且无效。

请注意:从5.3.5 / 5 :删除不完整的类型可能是未定义的行为

如果要删除的对象在删除时具有不完整的类类型,并且完整的类具有非平凡的析构函数或释放函数,则该行为是undefined

g++ 甚至发出以下警告:

警告:在调用删除运算符时可能检测到问题:
警告:“ p”具有不完整的类型
警告:正向声明了“结构C”


1
“删除不完整的类型是未定义的行为”是不正确的。如果类型具有非平凡的析构函数,则只有UB。这个小技巧解决的问题恰恰是删除不完整类型并不总是UB,因此语言支持它。
干杯和健康。-Alf 2015年

谢谢@ Cheersandhth.-Alf我的意思是,它可能是UB,所以总的来说,这行代码是UB。编辑。
Mohit Jain

32

sizeof(C)如果不是完整类型,将在编译时失败C。对其设置局部作用域enum可使语句在运行时变为良性。

这是程序员保护自己免受自身攻击的一种方式:delete ptr_如果后继对象具有非平凡的析构函数,则其对不完整类型的行为是不确定的。


1
您能否解释一下为什么C在那时需要是一个完整的类型-是否有必要使用完整的类型定义来调用delete它?如果是这样,编译器为何仍未捕获到它?
彼得·赫尔

1
是不是要避免C = void?如果C仅仅是未定义的类型,该delete语句是否不会失败?
Kerrek SB 2015年

1
看起来Mohit Jain有了答案。
彼得·赫尔

1
-1 “将本地范围枚举设置为它会使语句在运行时变为良性。” 是,毫无意义。对不起。
干杯和健康。-Alf 2015年

1
@SteveJessop谢谢。我缺少的部分是删除不完整的类型是UB。
彼得·赫尔

28

要了解enum,请首先考虑不带的析构函数:

~scoped_ptr() {
    delete ptr_;
}

哪里ptr_C*。如果此时的typeC不完整,即编译器知道的全部是struct C;,则(1)默认生成的空做什么析构函数用于所指向的C实例。对于由智能指针管理的对象,这不太可能是正确的选择。

如果通过指向不完整类型的指针进行删除始终具有未定义行为,则该标准可能仅要求编译器对其进行诊断并失败。但是,当真正的析构函数是微不足道的时,它是明确定义的:程序员可以拥有而编译器没有的知识。语言为什么定义并允许这样做,这超出了我的范围,但是C ++支持许多今天不被视为最佳实践的实践。

完整类型具有已知的大小,因此,sizeof(C)当且仅当C是完整类型(具有已知析构函数)时,才会编译。因此它可以用作警卫。一种方法就是简单

(void) sizeof(C);  // Type must be complete

猜想,使用某些编译器和选项,编译器会在对其不应该进行编译之前进行优化,enum从而避免了这种不符合规范的编译器行为:

enum { type_must_be_complete = sizeof(C) };

选择enum不只是丢弃的表达方式的另一种解释就是个人喜好。

或正如James T. Hugget在对此答案的评论中所建议的那样: “枚举可能是在编译时创建伪便携式错误消息的一种方式”。


(1)对于不完整类型,默认生成的空做什么析构函数是old的问题std::auto_ptr。它是如此阴险,以至于它进入国际C ++标准化委员会主席赫伯·萨特(Herb Sutter)撰写的有关PIMPL习惯用语的GOTW项目中。当然,在如今std::auto_ptr已弃用的情况下,将改为使用其他某种机制。


4
枚举可能是在编译时创建伪便携式错误消息的一种方式。
Brice M. Dempsey

1
我认为这个答案很好地解释了代码的动机,但我想补充一点:(1)一些编译器sizeof(T)对不完整类型的求值为0而不是使编译失败。但是,这是不合格的行为。(2)自C ++ 11起,使用static_assert((sizeof(T) > 0), "T must be a complete type");将是一种优越的(惯用的)解决方案。
5gon12eder 2015年

@ 5gon12eder; 请提供一个C ++编译器的示例,该示例的“sizeof(T)不完整类型的评估为0”。
干杯和健康。-阿尔夫

1
我从来没有使用过这样的编译器,听说它们现在已经消失了,我也不会感到惊讶。即使他们没有,不关心不合格的实施也是一个有效的决定。尽管如此,libstdc ++libc ++static_assert(sizeof(T) > 0, "…");在各自的实现中使用std::unique_ptr来确保类型完整……
5gon12eder 2015年

1
…所以我可以肯定地说这是惯用的。无论如何,由于sizeof(T)在布尔上下文中进行评估完全等同于测试sizeof(T) > 0,因此,除了出于美学原因,这并不重要。
5gon12eder

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.