如何处理C ++类构造函数中的失败案例?


21

我有一个CPP类,其构造函数执行一些操作。其中一些操作可能会失败。我知道构造函数不会返回任何东西。

我的问题是

  1. 除了初始化构造函数中的成员之外,是否可以执行其他操作?

  2. 是否可以告诉调用函数构造函数中的某些操作已失败?

  3. new ClassName()如果构造函数中发生某些错误,我可以使返回NULL吗?


22
您可以从构造函数中引发异常。这是一个完全有效的模式。
安迪

1
您可能应该看看GoF的一些创建模式。我推荐工厂模式。
SpaceTrucker

2
#1的常见示例是数据验证。也就是说,如果你有一个类Square,有一个构造函数的一个参数,一个边长,你要检查,如果该值大于0
大卫恢复莫妮卡说,

1
对于第一个问题,让我警告您,虚函数在构造函数中的行为可能不直观。与解构函数相同。当心这样称呼。

1
#3-为什么要返回NULL?OO的好处之一是不必检查返回值。只是catch()适当的潜在异常。
MrWonderful '16

Answers:


42
  1. 是的,尽管某些编码标准可能会禁止它。

  2. 是。推荐的方法是引发异常。或者,您可以将错误信息存储在对象内部,并提供访问此信息的方法。

  3. 没有。


4
除非即使构造函数参数的某些部分不符合要求并因此被标记为错误,否则该对象仍处于有效状态,否则实际上不建议这样做2)。最好是对象处于有效状态或根本不存在。
安迪

@DavidPacker同意,请参见此处:stackoverflow.com/questions/77639/…但是某些编码准则禁止异常,这对于构造函数来说是有问题的。
塞巴斯蒂安·雷德尔

塞巴斯蒂安,不知何故,我已经给了您这个答案的支持。有趣。:D
安迪

10
@ooxi不,不是。覆盖的new会被调用以分配内存,但是构造函数的调用是在操作符返回后由编译器完成的,这意味着您无法捕获错误。假设new完全被调用;它不是用于堆栈分配的对象,应该是大多数对象。
塞巴斯蒂安·雷德尔

1
对于#1,RAII是一个常见的示例,其中可能需要在构造函数中执行更多操作。
埃里克

20

您可以创建一个静态方法来执行计算,并在成功的情况下返回对象,或者在失败的情况下返回对象。

根据对象的这种构造方式,最好创建另一个允许以非静态方法构造对象的对象。

间接调用构造函数通常称为“工厂”。

这也将允许您返回空对象,这可能是比返回空值更好的解决方案。


谢谢@null!不幸的是在这里不能接受两个答案:((否则我也将接受这个答案!再次感谢!
MayurK '16

@MayurK无后顾之忧,接受的答案是没有标记正确答案,而是一个为你工作。
无效

3
@null:在C ++中,您不能只返回NULL。例如,int foo() { return NULL实际上您将返回0(零)一个整数对象。在std::string foo() { return NULL; }您不小心调用的情况下std::string::string((const char*)NULL),这是未定义的行为(NULL并不指向\ 0终止的字符串)。
MSalters

3
std :: optional可能还有一段距离,但如果您想这样做,可以始终使用boost :: optional。
肖恩·伯顿

1
@Vld:在C ++中,对象不限于类类型。而且,对于通用编程,最终以的工厂而告终int。例如,这std::allocator<int>是一个完美的理智工厂。
MSalters

5

@SebastianRedl已经给出了简单直接的答案,但是一些额外的解释可能会有用。

TL; DR =有一种样式规则可以使构造函数保持简单,这是有原因的,但是这些原因主要与历史性(或根本就是不好的)编码风格有关。构造函数中的异常处理已得到很好的定义,并且仍将为完全构造的局部变量和成员调用析构函数,这意味着惯用的C ++代码应该没有任何问题。样式规则仍然存在,但是通常这不是问题-并非所有初始化都必须在构造函数中,尤其是不一定在构造函数中。


这是一种常见的样式规则,即构造函数应尽其所能来设置已定义的有效状态的绝对最小值。如果您的初始化更为复杂,则应在构造函数之外进行处理。如果构造函数无法设置便宜的初始化值,则应减弱类强制执行的不变式以添加一个。例如,如果为类管理分配存储空间太昂贵,则添加一个尚未分配的null状态,因为当然,像null这样的特殊情况永远不会给任何人造成任何问题。啊

尽管很常见,但以这种极端的形式肯定不是绝对的。尤其是,正如我的讽刺所指出的那样,我在营地中说削弱不变性几乎总是代价太高。但是,样式规则背后有一些原因,并且有办法同时具有最少的构造函数强不变性。

原因与自动析构函数清除有关,尤其是在遇到异常时。基本上,当编译器负责调用析构函数时,必须有一个明确定义的点。当您仍在进行构造函数调用时,该对象不一定是完全构造的,因此调用该对象的析构函数是无效的。因此,只有在构造函数成功完成后,销毁对象的责任才转移到编译器。这就是RAII(资源分配是初始化),实际上并不是最好的名字。

如果在构造函数内部发生异常抛出,则需要显式清除部分构造的任何东西,通常是在try .. catch

然而,组件其已成功构建了对象已经是编译器的责任。这意味着实际上,这并不是什么大问题。例如

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

该构造函数的主体为空。只要是的构造函数base1member2并且member3它们是异常安全的,就没有什么可担心的。例如,如果member2throws 的构造函数,则该构造函数负责清理自身。该基础base1已经完全构建,因此将自动调用其析构函数。member3甚至从未进行过部分构造,因此不需要清理。

即使存在主体,在抛出异常之前已完全构造的局部变量也将像其他任何函数一样被自动破坏。处理原始指针或“拥有”某种隐式状态(存储在其他位置)的构造函数主体-通常意味着begin / acquire函数调用必须与end / release调用匹配-可能导致异常安全问题,但实际的问题在那里无法通过类正确管理资源。例如,如果unique_ptr在构造函数中用替换原始指针,unique_ptr则需要时将自动调用的析构函数。

人们还有其他原因喜欢偏爱最小的构造函数。一种是因为存在样式规则,所以许多人认为构造函数调用很便宜。拥有一个但仍具有很强不变性的方法是拥有一个单独的factory / builder类,该类具有弱化的不变性,并使用(可能有许多)正常的成员函数调用来设置所需的初始值。拥有所需的初始状态后,将该对象作为参数传递给具有强不变性的类的构造函数。这样可以“窃取”弱不变性对象的胆量-移动语义-这是一种廉价的(通常是noexcept)操作。

当然,您可以将其包装在一个make_whatever ()函数中,因此该函数的调用者无需查看弱化不变类实例。


您写的段落“虽然仍在进行构造函数调用,但对象不一定是完全构造的,因此调用该对象的析构函数是无效的。因此,破坏对象的责任只会转移到编译器当构造函数成功完成时”可以真正使用有关委托构造函数的更新。当最派生的构造函数完成时,对象将完全构建,如果委派的构造函数内部发生异常,则将调用析构函数。
Ben Voigt

因此,“ do-the-minimum”构造函数可以是私有的,而“ make_whatever()”函数可以是另一种调用私有函数的构造函数。
Ben Voigt

这不是我熟悉的RAII的定义。我对RAII的理解是有意在(仅在)对象的构造函数中获取资源,然后在其析构函数中释放该资源。通过这种方式,可以在堆栈上使用对象来自动管理对其封装的资源的获取和释放。典型的例子是一个锁,该锁在构造时会获取一个互斥锁,并在销毁时释放它。
埃里克

1
@Eric-是的,这绝对是标准做法-一种通常称为RAII的标准做法。延伸定义的不只是我-在某些谈话中甚至是Stroustrup。是的,RAII是关于将资源生命周期链接到对象生命周期的链接,思维模型是所有权。
Steve314 '16

1
@Eric-先前的回复已删除,因为对它们的解释不正确。无论如何,对象本身就是可以拥有的资源。一切都应main功能或静态/全局变量之前拥有所有者。使用分配的对象new拥有,直到您分配的责任,但智能指针拥有他们引用堆中分配的对象和容器拥有自己的数据结构。所有者可以选择早期删除,由所有者的析构函数最终负责。
Steve314 '16
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.