C ++中的错误处理,构造函数与常规方法

时间:2012-12-02 09:43:38

标签: c++

我有一个cheesesales.txt CSV文件,其中包含我最近的所有奶酪销售情况。我想创建一个可以执行以下操作的类CheeseSales

CheeseSales sales("cheesesales.txt"); //has no default constructor
cout << sales.totalSales() << endl;
sales.outputPieChart("piechart.pdf");

上述代码假定不会发生任何故障。实际上,失败将会发生。在这种情况下,可能会发生两种故障:

  • 构造函数失败:文件可能不存在,可能没有读取权限,包含无效/不可解析的数据等。
  • 常规方法失败:文件可能已存在,可能没有写入权限,可用于创建饼图的销售数据太少等。

我的问题很简单:您如何设计此代码来处理故障?

一个想法:从常规方法返回bool表示失败。不知道如何处理构造函数。

经验丰富的C ++程序员如何做这些事情?

7 个答案:

答案 0 :(得分:4)

在C ++中,异常是报告错误的方法。可以处理初始化列表中的BTW异常。

  

function-try-block将处理程序seq与   ctor-initializer(如果存在)和函数体。一个   在执行初始化表达式期间抛出的异常   在ctor-initializer中或在执行函数体期间   将控制转移到函数try-block中的处理程序中   作为在try-block执行期间抛出的异常的方式   将控制权转移给其他处理者。

好的代码通常应该在大多数上层(线程)级别使用最少的try / catch块。理想情况下只有一个。这样,知道“一切都抛出”,你就不会过多地考虑错误,而你的正常场景代码流看起来很干净。

答案 1 :(得分:2)

构造函数用于初始化对象的内部状态。不要用它来进行重读操作,如读取和处理文件。

使用一个方法来代替读取文件并抛出异常(或返回一个布尔值表示成功),以防错误发生。在主流程中捕获此异常并按您认为合适的方式处理它。

编辑:如果这是全班的目的,那么ChessSales可能只包含数据,您应该使用工厂类(或者静态实用程序类),它具有读取CSV文件并返回包含从CSV文件读取的相关数据的ChessSales对象的方法。这样您就可以将数据与业务逻辑分开(在这种情况下,读取和解析CSV文件)

答案 2 :(得分:2)

好吧,抛出异常是显而易见的选择。这在C ++中有一些问题,因为捕获initalizer列表构造函数抛出的异常是不可能的,这会导致各种麻烦。

因此,您实际应该提供访问该文件并可能抛出异常的构造函数,以及一个将对象保留为“未加载数据状态”的默认构造函数。这允许您将对象安全地用作其他类的成员,同时还允许另一个构造函数加载(或抛出异常)数据。

另一种选择是让数据加载构造函数不抛出异常,但是如果加载失败则将对象设置为无效状态,并使其他方法抛出异常,并且具有当前状态的getter。

在任何情况下,你都需要你班级的错误/未初始化状态,我相信没有安全的方法。

通过@MathieuM的注释进行编辑:在外部实现“未初始化状态”的一种替代方法是使其成为可选项,最容易使用指针包装类。然后在初始化程序列表中将其安全地初始化为NULL,并在构造函数体中尝试实际初始化,无论何种错误处理。通过选择它,你可以让构造函数抛出异常,让类的用户担心它。

答案 3 :(得分:2)

你辨别这两种失败是对的,事实上它们是微妙的不同。


  

构造函数中的失败:文件可能不存在,可能没有读取权限,包含无效/不可解析的数据等。

只是抛出异常。半成品对象是最常见的错误程序。

一个类应该具有不变,构造函数建立,并且所有方法维护。如果构造函数无法建立不变量,那么该对象是不可用的,并且在C ++中报告此对象的最佳方法是抛出异常,以便语言确保不会使用半构建对象。

如果有人建议您可能需要无效状态,请提醒他们单一责任原则:您的班级已经具有特定责任(其不变性),那些人希望更多可以封装它专用于提供选项的类。我的头脑,boost::optionalstd::unique_ptr都是很好的选择。


  

常规方法失败:文件可能已存在,可能没有写访问权限,可用于创建饼图的销售数据太少等。

不幸的是,您未能区分两种情况:

  • 只能读取实例的方法
  • 也修改实例的方法

对于所有方法,您需要选择错误报告策略。我的建议是例外 例外。如果失败被认为是例外(当网络链接达到99.99%的时间时,网络链路断开),那么例外是可以的。另一方面,如果预期失败,通常取决于输入(例如find方法或在您的情况下对指定文件的write方法),那么您希望为用户提供机会做出适当的反应。

在排除例外后,至少还有两种方法:

  • 返回代码(boolenum),表明操作是否顺利
  • 要求用户提供错误策略,以便在出现问题时调用

错误策略可能像enum一样简单(跳过,重试一次,抛出),也可能像使用各种方法的Strategy一样复杂。

此外,没有人说您的方法可能只有一个错误报告机制。例如,您可以选择:

  • 如果文件已经存在则调用错误策略(毕竟用户提供,他们可能想要切换到其他名称)
  • 如果无法访问磁盘,则抛出
  • (硬件通常可以正常工作!)

最后,除此之外,还修改实例的方法必须担心构造函数建立的维护不变。如果一个方法可能搞乱了不变量,那么它就没有任何不变量,并且每次使用该类时你的用户都应该担心地震......一般的智慧是执行所有可能在之前抛出的操作>开始修改对象。

简单(但非常简单)的实现是复制和交换习惯用法:内部复制对象,对副本执行操作,并在方法结束时将副本的状态与当前对象的状态。如果出现任何问题,副本将被破坏并在堆栈展开期间立即丢弃,从而保持对象不受影响。

有关此主题的更多信息,您可能需要阅读例外保证。我所描述的是强异常保证方法的典型实现,类似于数据库事务(全部或全部)。

答案 4 :(得分:0)

这是我处理错误的方法:我记录日志文件中的所有错误,并创建一个简单易用的函数来报告错误。如果程序运行不正常,我打开日志文件并找到原因。

关于如何在错误发生时了解错误:将异常引入C ++的原因之一是报告构造函数中的错误。由于构造函数不返回值,因此它们可以抛出异常并以这种方式报告失败。就个人而言,我不太喜欢这个方案,因为它会让你把你的代码放在try..catch之内。如果你忘记这样做,你可能想知道为什么你的程序崩溃了..

所以我通常会这样做:

1)让构造函数为成功/失败设置成员变量,具体取决于构造函数的成功操作。我可以稍后用if (myobj.constructor_ok()...)之类的东西来检查它。请注意,我没有直接访问成员变量。

2)我非常喜欢从方法返回true / false,尽可能表示成功/失败。这使代码非常容易阅读。

3)..和上面的日志文件。

答案 5 :(得分:0)

我认为您可以在构造对象之前从输入文件中获取信息。 例如,代码可能如下:

if(!getInfoFromFile("cheesesales.txt", date, amount, kindOfCheese, money)){
    cout << "Failed to get information from file." << endl;
    return FALSE;
}
CheeseSales sales(date, amount, kindOfCheese, money);
cout << sales.totalSales() << endl;
sales.outputPieChart("piechart.pdf");

然后你可以避免在构造函数中处理错误 当然,抛出异常也是一种解决方案。但我不喜欢它,因为c ++中的异常非常复杂。您可能会遇到许多难以想象的问题。

答案 6 :(得分:0)

在提出这个问题时,你错过了一个重要的细节:当操作失败时你想要发生什么?

我肯定会在构造函数的情况下抛出异常。我希望能像你上面写的那样编写代码;我不希望它看起来像

CheeseSales sales("cheesesales.txt"); //has no default constructor
if (!sales.good()) {
    // Somehow handle things here.
}
// The if block has to break control flow or reinitialise sales, otherwise
// these next lines will break.
cout << sales.totalSales() << endl;
sales.outputPieChart("piechart.pdf");

投掷意味着在这种情况下我可以假设更强的不变量,因此是一件好事。

看作输出操作不会改变CheeseSales的状态,抛出那里不会加强不变量。在这种情况下,使用辅助对象打印图表可能会更好:

ChooseSales sales("cheesesales.txt");
PdfStream chart("piechart.pdf");
chart << sales.asPieChart();

PdfStream可能只是std::ofstream,但您可能希望提供更多功能。)

如果操作失败,它可以将chart对象置于错误状态,这与iostream通常的工作方式相对应。