【问题标题】:Dynamic allocation of class array with protected destructor使用受保护的析构函数动态分配类数组
【发布时间】:2017-06-06 08:53:35
【问题描述】:

如果我定义了一个类

class A {
protected:
    ~A(){ }
};

然后我可以动态分配单个对象以及对象数组,例如

A* ptr1 = new A;
A* ptr2 = new A[10];

但是当我为这个类定义构造函数时

class A {
public:
    A(){}
protected:
    ~A(){ }
};

然后我可以创建单个对象

A* ptr = new A;

但是当我尝试动态分配对象数组时

A* ptr = new A[10];

编译器(gcc-5.1 和 Visual Studio 2015)开始抱怨 A::~A() 无法访问。

谁能解释一下:-

1- 为什么定义构造函数和未定义构造函数的行为差异。

2- 定义构造函数时,为什么允许我创建单个对象而不是对象数组。

【问题讨论】:

  • 刚刚用 clang 测试了这个(Apple LLVM 版本 8.0.0 clang-800.0.42.1)。它的行为与 gcc 不同,无论 A 是否具有显式声明的公共构造函数或所述构造函数是否为 noexcept,都会向 new A[10] 抱怨。

标签: c++ c++11 visual-c++ language-lawyer


【解决方案1】:

使用受保护的析构函数拒绝数组-new 是正确的,根据 C++11,§5.3.4 ¶17:

如果 new 表达式创建类类型的对象或对象数组,则对分配函数、解除分配函数 (12.5) 和构造函数 (12.1) 进行访问和歧义控制。 如果新表达式创建类类型的对象数组,则为析构函数 (12.4) 完成访问和歧义控制。

(添加了重点;C++03 中使用了几乎完全相同的措辞,§5.3.4 ¶16;C++14 移动了一些东西,但这似乎并没有改变问题的核心 -请参阅@Baum mit Augen 的回答)

这是因为new[] 仅在所有元素都已构造完成的情况下才会成功,并且希望避免在其中一个构造函数调用失败的情况下泄漏对象;因此,如果它设法构造了前 9 个对象,但第 10 个对象因异常而失败,则它必须在传播异常之前破坏前 9 个对象。

请注意,如果构造函数声明为noexcept,则逻辑上不需要此限制,但标准似乎在这方面没有任何例外。


所以,这里的 gcc 在第一种情况下在技术上是错误的,就标准而言,它也应该被拒绝,尽管我认为“道德上” gcc 做了正确的事情(实际上有A 的默认构造函数不可能抛出)。

【讨论】:

  • 感谢您挖掘我们的标准。这正是我所怀疑的:标准中的缺陷
  • @Walter:该标准不支持人们想要做的所有事情,如果支持,技术上可以做到。这不是缺陷,而是可能的缺点。这是主观的,因为对于其他人来说,避免标准中的更多复杂性可能更有价值。一个类似的缺点:该标准不支持反变体参数。同样的论点也适用:尽管有些人可能想要它,但对其他人来说,这只会增加复杂性以获得边际(如果有的话)收益。
  • 关于标准措辞,拥有noexcept 构造函数可能不足以保证在构造数组的过程中不会抛出异常。构造函数可能有一个带有调用中使用的默认参数的参数,并且该初始化可能会抛出。我不认为处理像问题中的示例这样的简单案例值得增加措辞的复杂性。此外,此处指定的方式与 [class.base.init] 中处理其他类型的子对象的方式一致。抄送@Walter
  • @bogdan 一致,但是了解 default 构造函数如何具有参数?如果一个类有一个构造函数the_class::the_class() noexcept并且它抛出了,那么terminate()必须被调用。
  • @Walter the_class::the_class(other_class = other_class{}) noexcept 是根据[class.ctor]/4 的默认构造函数。
【解决方案2】:

事实证明,gcc 在这里是不正确的。在 N4141 (C++14) 中,我们有:

如果 new-expression 创建一个类类型的对象数组,析构函数可能会被调用 (12.4)。

(5.3.4/19 [expr.new]) 和

一个 如果可能调用的析构函数被删除或无法从上下文访问,则程序格式错误 调用。

(12.4/11 [class.dtor])。 所以这两种数组情况都应该被拒绝。(Clang 确实做到了,live。)

原因是,正如其他人和我的旧错误答案所提到的那样,类类型元素的构造可能会因异常而失败。发生这种情况时,必须调用所有完全构造的元素的析构函数,因此析构函数必须是可访问的。

当使用operator new(没有[])分配单个元素时,该限制不适用,因为如果单个构造函数调用失败,则不可能有完全构造的类实例。

【讨论】:

  • 为什么new A[10];中的析构函数可能在A::A() noexcept;时被调用?
  • @Walter Standard 这么说。 vOv 如果您可以根据noexcept 为逻辑上有效的异常提出一个好的用例,您可以写一个提案来改变它。
【解决方案3】:

我不是语言律师(对标准非常熟悉),但怀疑答案与 Baum mit Augen 之前给出的答案一致(已删除,因此只有具有足够声誉的人才能看到它)。

如果后续数组元素构造失败并抛出异常,则需要删除已经构造的元素,需要访问析构函数。

但是,如果构造函数是noexcept,则可以排除这种情况,不需要访问析构函数。即使在这种情况下 gcc 和 clang 仍然会抱怨,这很可能是一个编译器错误。也就是说,编译器没有考虑到构造函数是noexcept。或者,编译器可能在标准内,在这种情况下,这听起来像是标准中的缺陷

【讨论】:

    猜你喜欢
    • 2018-06-15
    • 2012-02-29
    • 2013-05-18
    • 1970-01-01
    • 1970-01-01
    • 2013-11-03
    • 2012-02-05
    • 2011-05-30
    • 2012-11-23
    相关资源
    最近更新 更多