你对这两种失败的区分是对的,它们确实有细微的不同。
构造函数失败:文件可能不存在,可能没有读取权限,包含无效/不可解析的数据等。
只是抛出一个异常。半构建对象是解决程序错误的最短方法。
一个类应该有一个构造函数建立并且所有方法都维护的不变量。如果构造函数无法建立不变量,则对象不可用,在 C++ 中报告这一点的最佳方法是抛出异常,以便语言确保不使用半构建的对象。
如果人们建议您可能想要一个无效状态,请提醒他们单一职责原则:您的类已经具有特定的职责(它的不变量),希望更多的人可以将其封装在一个专门提供可选性的类中。离开我的头,boost::optional
都是std::unique_ptr
很好的选择。
常规方法失败:文件可能已经存在,可能没有写入权限,可用于创建饼图的销售数据太少等。
不幸的是,您未能区分两种情况:
对于所有方法,您需要选择错误报告策略。我的建议是例外是例外的。如果故障被认为是异常的(网络链接在 99.99% 的时间都在运行时断开),那么异常就可以了。另一方面,如果预计会失败,通常取决于输入(例如find
方法或在您的情况下write
是指定文件的方法),那么您希望让用户有机会做出适当的反应。
在排除了例外之后,至少还有两种方法:
- 返回一个代码 (
bool
, enum
) 指示操作是否顺利
- 要求用户提供将在出现问题时调用的错误策略
错误策略可能像enum
(跳过、重试、抛出)一样简单,也可能像Strategy
各种方法一样复杂。
此外,没有人说您的方法可能只有一种错误报告机制。例如,您可以选择:
- 如果文件已经存在,则调用错误策略(毕竟是用户提供的,他们可能想要切换到不同的名称)
- 如果无法访问磁盘,则抛出(通常预计硬件可以工作!)
最后,除此之外,也修改实例的方法必须担心维护构造函数建立的不变量。如果一个方法可能会搞砸不变量,那么它根本就不是不变量,每次使用该类时,您的用户都应该感到恐惧……一般的智慧是在开始修改对象之前执行所有可能抛出的操作。
一个简单(但非常容易)的实现是复制和交换习惯用法:在内部复制对象,对副本执行操作,并在方法结束时将副本的状态与当前对象的状态交换。如果出现任何问题,副本将被损坏并在堆栈展开期间立即丢弃,而对象不受影响。
有关此主题的更多信息,您可能需要阅读Exceptions Guarantees。我描述的是强异常保证方法的典型实现,类似于数据库事务(全有或全无)。