GetLastError()是一种设计模式吗?这是好机制吗?

时间:2012-02-01 17:04:01

标签: c++ winapi design-patterns coding-style mfc

Windows API使用GetLastError()机制来检索有关错误或失败的信息。我正在考虑使用相同的机制来处理错误,因为我正在为专有模块编写API。我的问题是API更好地直接返回错误代码? GetLastError()有什么特别的优势吗?考虑下面的简单Win32 API示例:

HANDLE hFile = CreateFile(sFile,
    GENERIC_WRITE, FILE_SHARE_READ,
    NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);

if (hFile == INVALID_HANDLE_VALUE)
{
    DWORD lrc = GetLastError();

    if (lrc == ERROR_FILE_EXISTS)
    {
          // msg box and so on
    }
}

当我编写自己的API时,我意识到GetLastError()机制意味着CreateFile()必须在所有出口点设置最后一个错误代码。如果有许多退出点并且其中一个可能错过,则这可能会有一点容易出错。愚蠢的问题,但是它是如何完成的,或者有某种设计模式呢?

另一种方法是为函数提供一个额外的参数,它可以直接填写错误代码,因此不需要单独调用GetLastError()。另一种方法可以如下。我将坚持使用上面的Win32 API,这是分析器的好例子。在这里,我将格式更改为此(假设)。

result =  CreateFile(hFile, sFile,
    GENERIC_WRITE, FILE_SHARE_READ,
    NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);

if (result == SUCCESS)
{
   // hFile has correct value, process it
}
else if (result == FILE_ALREADY_EXIT )
{
   // display message accordingly
  return;
}
else if ( result == INVALID_PATH )
{
   // display message accordingly.
  return;
}

我的最终问题是从API或甚至只是函数返回错误代码的首选方法是什么,因为它们都是相同的?

7 个答案:

答案 0 :(得分:20)

总的来说,这是一个糟糕的设计。这不是Windows GetLastError函数特有的,Unix系统与全局errno变量具有相同的概念。这是因为它是隐含的函数的输出。这有一些令人讨厌的后果:

  1. 同时执行的两个函数(在不同的线程中)可能会覆盖全局错误代码。因此,您可能需要具有每线程错误代码。正如对此答案的各种评论所指出的,这正是GetLastErrorerrno所做的 - 如果您考虑为您的API使用全局错误代码,那么您需要执行相同的操作您的API应该可以在多个线程中使用。

  2. 如果外部函数覆盖内部设置的错误代码,则两个嵌套函数调用可能会丢弃错误代码。

  3. 忽略错误代码非常容易。事实上,实际上很难记住它存在,因为不是每个函数都使用它。

  4. 自己实现功能时很容易忘记设置它。可能存在许多不同的代码路径,如果您不注意,其中一个可能允许控制流转义而无需正确设置全局错误代码。

  5. 通常,错误条件是例外。它们并不经常发生,但它们可以。您需要的配置文件可能无法读取 - 但大部分时间都是如此。对于此类异常错误,您应该考虑使用C ++异常。任何值得它的C ++书籍都会列出为什么任何语言(不仅仅是C ++)中的异常都是好的原因,但在激动人心之前有一个重要的事情需要考虑:

    例外展开堆栈。

    这意味着当你有一个产生异常的函数时,它会传播给所有调用者(直到它被某人捕获,可能是C运行时系统)。这反过来会产生一些后果:

    1. 所有调用者代码都需要知道异常的存在,因此所有获取资源的代码必须能够释放它们,即使面对异常(在C ++中,'RAII'技术通常用于解决它们。)

    2. 事件循环系统通常不允许异常转义事件处理程序。在这种情况下,处理它们没有好的概念。

    3. 处理回调的程序(例如普通函数指针,甚至是Qt库使用的'signal& slot'系统)通常不希望被调用函数(槽)产生异常,所以他们不打算试图抓住它。

    4. 底线是:如果您知道他们在做什么,请使用例外。因为你似乎对这个话题比较新,所以现在坚持返回函数的代码,但请记住,这通常不是一个好的技巧。在任何一种情况下都不要使用全局错误变量/函数。

答案 1 :(得分:7)

GetLastError模式是迄今为止最容易出错并且最不喜欢的模式。

到目前为止,返回状态代码enum是更好的选择。

你没有提到的另一个选项,但很受欢迎,就是为失败案例抛出异常。如果你想做正确的事情(而不是泄漏资源或让对象处于半设置状态),这需要非常仔细的编码,但是会产生非常优雅的代码,其中所有核心逻辑都在一个地方并且错误处理整齐地分开了。

答案 2 :(得分:3)

我认为GetLastError是多线程前几天的遗物。我不认为该模式应该再次使用,除非错误非常罕见。问题是错误代码必须是每个线程。

GetLastError的另一个不满是它需要两个级别的测试。首先必须检查返回代码以查看它是否表示错误,然后您必须调用GetLastError来获取错误。这意味着你必须做两件事之一,既不特别优雅:

1)您可以返回表示成功或失败的布尔值。但是,为什么不将错误代码返回零以获得成功呢?

2)您可以根据非法作为主要返回值的值对每个函数进行不同的返回值测试。但那么任何返回值合法的函数是什么?这是一个非常容易出错的设计模式。 (零是某些函数的唯一非法值,因此在这种情况下你会返回零错误。但是如果零是合法的,你可能需要使用-1或其他一些。很容易让这个测试错误。)

答案 3 :(得分:3)

我不得不说,我认为当不能使用异常处理时,全局错误处理程序样式(具有适当的线程本地存储)是最现实的。这肯定不是最佳解决方案,但我认为如果你生活在我的世界(一个懒惰的开发人员的世界,他们不经常检查错误状态),这是最实用的。

理由:开发人员倾向于不经常检查错误返回值。我们可以在实际项目中指出多少个例子,其中函数只返回一些错误状态,只有调用者才能忽略它们?或者有多少次我们看到一个甚至没有正确返回错误状态的函数,即使它是分配内存(可能会失败的东西)?我已经看过太多像这样的例子,回过头来修复它们有时甚至需要通过代码库进行大规模的设计或重构更改。

在这方面,全局错误处理程序更加宽容:

  • 如果函数未能返回布尔值或某些ErrorStatus类型来指示失败,我们不必修改其签名或返回类型以指示失败并在整个应用程序中更改客户端代码。我们可以修改其实现来设置全局错误状态。当然,我们仍然需要在客户端添加检查,但是如果我们在呼叫站点立即错过了错误,那么以后仍有机会抓住它。

  • 如果客户端无法检查错误状态,我们仍然可以在以后捕获该错误。当然,错误可能会被后续错误覆盖,但是我们仍然有机会在某个时刻看到发生了错误,而调用简单地忽略调用站点错误返回值的代码将永远不允许稍后会发现错误。

虽然是次优解决方案,但如果无法使用异常处理,并且我们正在与一群习惯于忽略错误返回值的代码猴子一起工作,这是迄今为止最实用的解决方案正如我所见。

当然,具有适当异常安全性(RAII)的异常处理是迄今为止优越的方法,但有时不能使用异常处理(例如:我们不应该抛弃模块边界)。虽然像Win API的 GetLastError 或OpenGL的 glGetError 这样的全局错误处理程序听起来像是从严格的工程角度来看的劣质解决方案,但是对系统进行改造比对系统进行改造要宽松得多。开始让一切都返回一些错误代码并开始强制调用这些函数的所有内容来检查它们。

但是,如果应用此模式,则必须注意确保它可以在多个线程中正常工作,并且不会显着降低性能。我实际上必须设计我自己的线程本地存储系统才能做到这一点,但我们的系统主要使用异常处理,只有这个全局错误处理程序将跨模块边界的错误转换为异常。

总而言之,异常处理是要走的路,但如果由于某些原因这是不可能的,我不得不在这里不同意大多数答案,并为较大的,不那么自律的团队提出类似GetLastError的建议(I如果忽略返回的错误状态,这使得我们至少可以在以后注意到错误,并且允许我们将错误处理改进为一个错误处理,从而通过调用堆栈为更小,更有纪律的错误返回错误。通过简单地修改其实现而不修改接口而没有正确设计来返回错误的函数。

答案 4 :(得分:3)

如果您的API在DLL中并且您希望支持使用不同编译器的客户端,那么您就不能使用异常。例外没有二进制接口标准。

所以你几乎必须使用错误代码。但是,不要使用GetLastError作为示例来建模系统。如果你想要一个如何返回错误代码的好例子,请看COM。每个函数都返回HRESULT。这允许调用者编写可以将COM错误代码转换为本机异常的简洁代码。像这样:

Check(pIntf->DoSomething());

其中Check()是由您编写的函数,它接收HRESULT作为其单个参数,并在HRESULT表示失败时引发异常。事实上,函数的返回值指示允许这种更简洁编码的状态。想象一下通过参数返回状态的替代方法:

pIntf->DoSomething(&status);
Check(status);

或者更糟糕的是,在Win32中完成它的方式:

if (!pIntf->DoSomething())
    Check(GetLastError());

另一方面,如果您准备要求所有客户端使用与您相同的编译器,或者您将库作为源提供,则使用例外。

答案 5 :(得分:1)

不建议在非托管代码中进行异常处理。处理内存泄漏没有例外是一个大问题,除了它成为噩梦。

错误代码的线程局部变量并不是一个坏主意,但正如其他一些人所说的那样容易出错。

我个人更喜欢每种方法都返回错误代码。这会给功能方法带来不便,因为而不是:

int a = foo();

你需要写:

int a;
HANDLE_ERROR(foo(a));

这里HANDLE_ERROR可以是一个宏,用于检查从foo返回的代码,如果是错误,则将其传播(返回)。

如果您准备好一组宏来处理不同的情况,那么可以在没有异常处理的情况下使用良好的错误处理来编写代码。

现在,当您的项目开始增长时,您会注意到错误的调用堆栈信息非常重要。您可以扩展宏以将调用堆栈信息存储在线程本地存储变量中。这非常有用。

然后您会注意到即使是调用堆栈也不够。在许多情况下,在“fopen(path,...)”行的“找不到文件”的错误代码;没有给你足够的信息来找出问题所在。哪个是找不到的文件。此时,您可以扩展您的宏以便能够存储按摩。然后你可以提供未找到的文件的实际路径。

问题是为什么你可以用异常来解决所有这些问题。有几个原因:

  1. 同样,非托管代码中的异常处理很难做到
  2. 基于宏的代码(如果完成写入)恰好比异常处理所需的代码更小更快
  3. 这样更灵活。您可以启用禁用功能。
  4. 在我正在工作的项目中,我实现了这样的错误处理。花了2天的时间才准备开始使用它。大约一年后,我可能会花费大约2周的时间来维护和添加功能。

答案 6 :(得分:0)

您还应该考虑基于对象/结构的错误代码变量。就像stdio C库正在为FILE流做它。

例如,在我的一些io对象上,我只是在设置错误状态时跳过所有进一步的操作,以便用户在一系列操作后检查错误时就可以了。

此模式允许您更好地微调错误处理方案。

在比较谷歌GO语言时,其中一个不好的C / C ++设计就是充分发挥作用。从函数中返回一个值。 GO不使用异常,而是始终返回两个值,即结果和错误代码。

有一小部分人认为异常大部分时间都是坏的和误用的,因为错误不是例外,而是你必须要做的事情。并没有证明软件更可靠,更容易。特别是在C ++中,现在唯一的编程方法是RIIA技术。