【问题标题】:Is GetLastError() kind of design pattern? Is it good mechanism?GetLastError() 是一种设计模式吗?是好的机制吗?
【发布时间】:2012-02-24 09:05:11
【问题描述】:

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 甚至函数返回错误代码的首选方式是什么,因为它们都是相同的?

【问题讨论】:

  • 在这种情况下,switch 语句似乎更合适。
  • 为什么不使用异常?
  • 据我了解,您问的是通过返回码处理错误是否是个好主意。好吧,我更喜欢异常,它们使错误处理更加舒适。检查this question
  • “在我编写自己的 API 时,我意识到 GetLastError() 机制意味着 CreateFile() 必须在所有退出点设置最后一个错误代码。”不是这样。如果您已被告知(通过返回值)它实际上已被设置,您应该只检查GetLastError。在所有其他时间,它将是未定义的,无论它上次设置为什么。
  • @Deanna:一个很好的例子说明为什么 GetLastError 风格的错误机制使用起来很痛苦——它很容易不一致。

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


【解决方案1】:

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

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

另一个您没有提到但非常流行的选项是为失败案例抛出异常。如果您想正确执行此操作(并且不泄漏资源或使对象处于半设置状态),这需要非常仔细的编码,但会导致看起来非常优雅的代码,其中所有核心逻辑都在一个地方并且错误处理被整齐地分开了。

【讨论】:

  • 如果你使用“普通”的 C++ 技术(即 RAII 和阅读标准库),异常很容易使用。我同意这个隐含的事实,即 C++ 的大部分困难来自编写异常安全代码的需要。
  • 我不太确定外部库(在我的情况下是 dll)是否应该向外界抛出异常。首先,库 API 应该是通用的,最好是 C 风格,所以任何人都可以使用它,但即使它是 C++,通过使用异常,我将强制客户端应用程序也使用/处理这些异常,这(我认为)不是很常见的(对于我的图书馆的用户来说是更先进的工作)。我认为异常可以在我的库中使用,但不能暴露在它之外?这给我们留下了我最初的问题,如何返回错误。
  • 如果您正在编写一个供外部用户使用的库,异常将是糟糕的形式。许多地方都避开异常处理(包括我工作的地方!),他们可以仅凭此就完全拒绝您的库。我并不是说例外对每个人都适用,但是如果不提及现代错误处理,那将是不完整的。
【解决方案2】:

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

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

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

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

【讨论】:

  • @MooingDuck 当然,错误代码存储为特定于线程的数据,这使得每次访问它都变得更加尴尬和昂贵。 (在 UNIX 世界中也发生了同样的事情。errno 变量早于线程,因此必须针对线程。在 pthreads API 等较新的 API 中使用更明智的方法。)
  • 线程本地存储访问在 windows、linux 和 solaris 上的开销为 0,因为它只是相对于 FS 段寄存器而不是 DS 或 SS。并且全局变量访问也不会更尴尬。
【解决方案3】:

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

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

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

  3. 很容易忽略错误代码。事实上,更难真正记住它的存在,因为并非每个函数都使用它。

  4. 当你自己实现一个函数时很容易忘记设置它。可能有许多不同的代码路径,如果您不注意其中一个可能会在没有正确设置全局错误代码的情况下让控制流逃逸。

通常,错误情况是例外的。它们不会经常发生,但可以。您需要的配置文件可能不可读 - 但大多数情况下它是可读的。对于此类异常错误,您应该考虑使用 C++ 异常。任何值得一读的 C++ 书籍都会列出为什么任何语言(不仅仅是 C++)中的异常都是好的原因,但在兴奋之前有一件重要的事情需要考虑:

异常展开堆栈。

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

  1. 所有调用者代码都需要意识到异常的存在,因此所有获取资源的代码即使在遇到异常时也必须能够释放它们(在 C++ 中,“RAII”技术通常用于解决它们)。

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

  3. 处理回调的程序(例如普通函数指针,甚至 Qt 库使用的“信号和槽”系统)通常不期望被调用的函数(槽)会产生异常,所以他们不会费心去抓它。

底线是:如果您知道异常在做什么,请使用它们。由于您似乎对该主题相当陌生,因此暂时坚持返回函数代码,但请记住,这通常不是一个好的技术。在任何一种情况下都不要使用全局错误变量/函数。

【讨论】:

  • errno 被设计成一个扩展为一些线程本地错误代码的宏。不过总的来说你是对的。
  • Windows 相同 - 'GetLastError' 返回每个线程的值。如果没有,它将无法使用。
  • 您对 GetLastError 的许多批评都是虚假的。它是本地线程。嵌套函数?您必须检查每个 API 调用的错误,嵌套是无关紧要的。很容易忽略错误代码?一个以无异常语言为目标的 API 只能使用错误代码。
  • @DavidHeffernan:您写道“必须检查每个 API 调用的错误”——但并非每个 API 调用都会设置最后一个错误!考虑 Windows 套接字 API。这是一个糟糕的设计,不管是什么语言。如果它是返回值的一部分会更好 - 这样,至少每个函数的输入/输出都清晰可见。设计的全部是让正确的事情变得容易,让不正确的事情变得困难。全局值并不擅长于此。
  • “通常,错误情况是异常的”。我相信使用异常报告失败的最有用的属性是函数提供成功作为后置条件。如果代码在方法执行后立即执行,您就知道该方法已经完成了它的工作。无需对空值、超出范围的值、空缓冲区等进行冗长而复杂的防御性检查。
【解决方案4】:

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

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

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

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

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

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

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

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

总而言之,异常处理是要走的路,但如果由于某种原因无法做到这一点,我不得不不同意这里的大多数答案,并建议像 GetLastError 这样的大型、纪律性较差的团队(我'会说通过调用堆栈返回错误以获取更小、更规范的错误)基于如果返回的错误状态被忽略,这允许我们至少在以后注意到一个错误,并且它允许我们将错误处理改进为未正确设计为通过简单地修改其实现而不修改接口来返回错误的函数。

【讨论】:

  • "如果客户端检查错误状态失败,我们仍然可以稍后捕获错误。"仅当每个函数(无论是否使用此方法返回错误)都需要在成功调用期间保留错误状态时,这才是正确的。据我所知,没有人 这样做。 (例如,printf 允许修改存储的错误代码。)这种方法的一个巨大缺点是您以后无法轻松检查错误代码。
  • 我们也不能使用基于返回错误值的方法。我来自的世界是人们经常写 f() 而不是 if (f()) ... 或 ErrorStatus status = f(); 之类的世界。 if (status !=success) ... 当在调用站点检查错误失败时,错误将无可救药地丢失。后面十行代码我们都找不到出错的地方:我们一无所知,一头雾水。如果我们使用 OpenGL 之类的库,人们很少会在每次 gl 调用后检查错误。但是我们稍后可以发现发生了GL错误,并最终到达调用站点。
  • 这就是我的意思。使用类似于 GL 的方法,您至少可以在稍后的某个地方发现发生了错误,即使人们在每次 GL 调用后都没有检查。通过错误检查失败并不需要我们返回并重写所有内容以开始检测错误的来源,这是我更喜欢的一种妥协(诚然次优)的方法,因为它更宽容感觉。当然,尽可能地进行异常处理。
  • 继续,让我们假设 printf 覆盖了原始错误。假设我们懒惰、马虎的程序员既没有检查原始错误,也没有检查 printf 错误。至少我可以在某种程度上进入并检测到最后一个错误,即 printf 错误,然后开始追根究底。至少惰性编程并没有掩盖所有错误,就像在错误返回码被忽略的情况下一样。不同的是,最后一个错误仍然有待我们检测,即使它被覆盖,我们至少也发现了一个错误,而不是没有。
  • 很高兴看到一种方法的优缺点,感谢您的洞察力,非常感谢。当我编写 API 时,我的直觉是我不喜欢使用 GetLastError() 方法,因为它有点“分散”并且看起来不是模块化的。
【解决方案5】:

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

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

Check(pIntf->DoSomething());

其中Check() 是您编写的函数,它接收HRESULT 作为其单个参数,如果HRESULT 指示失败,则引发异常。正是函数的返回值指示了允许这种更简洁编码的状态。想象一下通过参数返回状态的替代方案:

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

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

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

另一方面,如果您准备要求所有客户端都使用与您相同的编译器,或者您将库作为源代码提供,那么请使用异常。

【讨论】:

  • 在 windows 上有一个标准 - 只有 unix 再次落后。 SEH 在 C 和 C++ 之间是兼容的,并且每个自称为 windows 兼容的编译器都必须以一种或另一种方式支持 SEH,因为基本的 windows 函数也会引发 SEH 异常。
  • @Lothar:很抱歉唤醒了一个老问题,但我不得不不同意“unix 再次落后光年”部分。一方面,标准 C 甚至没有 SEH。这意味着它是一个编译器扩展。在 Linux 上,二进制兼容性甚至不是什么大问题。几乎所有东西都以二进制和源代码的形式分发。请注意:我们都知道上次微软忽视标准时发生了什么。即。
  • @Linuxios 我同意。 SEH 在这里是一个红鲱鱼。标准 Windows API 使用 GetLastError 或 HRESULT。不是 SEH。
  • @DavidHeffernan:没错。虽然 MFC、COM、OLE 或更多基于 C++ 的东西可能会,但 Win32 肯定不会。
【解决方案6】:

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

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

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

在将 C/C++ 与谷歌的 GO 语言进行比较时,C/C++ 的一个糟糕设计在这里得到了充分体现。函数只返回一个值。 GO 不使用异常,而是总是返回两个值,结果和错误代码。

有一小部分人认为异常在大多数情况下都是不好的和被滥用的,因为错误不是异常,而是您必须预料到的。而且还没有证明软件变得更可靠和更容易。尤其是在 C++ 中,如今唯一的编程方法是 RIIA 技术。

【讨论】:

    【解决方案7】:

    不推荐在非托管代码中处理异常。无异常处理内存泄漏是个大问题,有了异常就成了噩梦。

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

    我个人更喜欢每种方法都返回错误代码。这给函数式方法带来了不便,因为不是:

    int a = foo();
    

    你需要写:

    int a;
    HANDLE_ERROR(foo(a));
    

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

    如果你准备了一组好的宏来处理不同的情况,那么在没有异常处理的情况下,具有良好错误处理能力的代码是可能的。

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

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

    问题是为什么要打扰所有这些你可以用例外来做的事情。几个原因:

    1. 同样,非托管代码中的异常处理很难正确处理
    2. 基于宏的代码(如果已完成写入)恰好比异常处理所需的代码更小且更快
    3. 它更加灵活。您可以启用禁用功能。

    在我目前正在工作的项目中,我实现了这种错误处理。我花了 2 天的时间才准备好开始使用它。在大约一年的时间里,我可能总共花费了大约 2 周的时间来维护和添加功能。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-10-07
      • 2017-10-18
      • 2023-03-29
      • 1970-01-01
      • 1970-01-01
      • 2021-06-29
      • 1970-01-01
      相关资源
      最近更新 更多