为什么 std::string 构造函数会重置 GetLastError

4

我正在从C++代码中调用Windows API,并且有一个帮助方法来执行FormatMessage的操作并抛出异常以进行错误处理。该函数的签名为:

void throw_system_error(const std::string& msg_prefix, DWORD messageid)

我注意到了一些奇怪的事情。这段代码不能正常工作:

handle = ...;
if (handle == NULL) {
    throw_system_error("something bad happened", GetLastError());
}

传递给 throw_system_error 的错误代码始终为零。

而这个可以正常工作:

handle = ...;
if (handle == NULL) {
    DWORD error = GetLastError();
    throw_system_error("something bad happened", error);
}

进一步调查发现,此版本存在相同的问题:

handle = ...;
if (handle == NULL) {
    std::string msg("something bad happened");
    DWORD error = GetLastError();
    throw_system_error(msg, error);
}

看起来就像是std::string的构造函数正在重置错误代码。

我的猜测是std::string在内部分配内存,这会导致某些系统调用,然后将最后一个错误代码设置为零。

有人知道这里实际发生了什么吗?

Visual C++ 2015,64位。


1
你的代码出了问题,就这么简单。在调用任何其他 API 函数之前,请先调用 GetLastError。显然,分配内存的调用可能会调用 SetLastError。我相当确定这个问题以前已经在这里问过了。 - David Heffernan
代码出了问题,但我想找到根本原因,而不是修复方法。更神秘的是,我已经测试了HeapAlloc,它并没有修改last-error。但是malloc将其设置为零。这在msdn上的文档中没有记录,因为GetLastError的文档要求如此。那么...我应该接受哪个答案呢? - Stefan
1
malloc超出了MSDN Win32文档的范围。它可以随心所欲地做任何事情。 - David Heffernan
你可能想考虑抛出一个仅由错误代码构造的 std::system_error。如果你需要在任意子系统边界添加字符串前缀,你可以在接口实现的适当 catch 子句中实现相应的代码。 - IInspectable
3个回答

6

让我们看一下GetLastError文档:

大多数设置线程的最后错误代码的函数在失败时设置它。然而,有些函数在成功时也设置最后错误代码。

当函数的返回值表明这样的调用将返回有用数据时,应立即调用GetLastError函数。这是因为一些函数在成功时使用零调用SetLastError,清除了最近失败函数设置的错误代码。

所以有一个函数调用了SetLastError,很可能是分配内存的函数:当构造字符串时,调用new来分配内存。

现在,让我们看一下vc++中new的实现。在Stack Overflow上有一个非常好的答案: https://softwareengineering.stackexchange.com/a/293209

这取决于您是处于调试模式还是发布模式。在发布模式下,有HeapAlloc / HeapFree等内核函数,而在调试模式下(使用Visual Studio),有手写的free和malloc版本(被重定向到new/delete),带有线程锁和更多异常检测,以便在调试模式下运行代码时更容易检测出堆指针错误。

因此,在发布模式下,调用的函数是HeapAlloc,该函数不会调用SetLastError。根据文档:

如果函数失败,它不会调用SetLastError

因此,代码应在发布模式下正常工作。 但是,在调试实现中,将调用FlsGetValue函数,当成功时该函数调用SetLastError

这很容易检查,

#include <iostream>
#include <Windows.h>
int main() {

    DWORD t = FlsAlloc(nullptr);
    SetLastError(23); //Set error to 23
    DWORD error1 = GetLastError(); //store error

    FlsGetValue(t); //If success, it is going to set error to 0

    DWORD error2 = GetLastError(); //store second error code

    std::cout << error1 << std::endl;
    std::cout << error2 << std::endl;
    system("PAUSE");
    return 0;
}

它输出以下内容:
23
0

So FlsGetValue 调用了 SetLastError()。为了证明它仅在调试时被调用,我们可以进行以下测试:

#include <iostream>
#include <Windows.h>
int main() {

    DWORD t = FlsAlloc(nullptr);
    SetLastError(23); //Set error to 23
    DWORD error1 = GetLastError(); //store error

    int* test = new int; //allocate int

    DWORD error2 = GetLastError(); //store second error code

    std::cout << error1 << std::endl; //output errors
    std::cout << error2 << std::endl;

    delete test; //free allocated memory
    system("PAUSE");
    return 0;
}

如果您在调试模式下运行它,它会给您提供信息,因为它调用了FlsGetValue函数:
23
0

然而,如果您在发布模式下运行它,它会调用HeapAlloc并生成:

23
23

你误解了HeapAlloc的文档说明。当它说“如果函数失败,它不会调用SetLastError。”时,它仅意味着在失败时调用GetLastError将返回一个不确定的值。这并不意味着API调用将保留调用线程的上一个错误。 - IInspectable

4
根据 GetLastError 的文档:
每个设置最后一个错误代码的函数的“返回值”部分都说明了在哪些条件下该函数设置最后一个错误代码。大多数设置线程的最后一个错误代码的函数在失败时设置它,但有些函数成功时也会设置最后一个错误代码。如果该函数未记录设置最后一个错误代码,则此函数返回的值仅是曾经设置过的最近的最后一个错误代码;一些函数在成功时将最后一个错误代码设置为0,而其他函数则不会这样做。 在构造 std::string 的过程中,调用了 SetLastError。Windows 上的标准库使用 Win32 调用作为其实现的一部分。
您的第二种方法(有效的)是正确使用 GetLastError 的方式
应当在函数的返回值表明这样的调用会返回有用数据时立即调用 GetLastError 函数。这是因为某些函数在成功时会调用 SetLastError,并将前一次失败函数设置的错误代码清除为零。

是的,我可以猜到这一点。但是在我所知道的std::string文档中,它并没有记录设置最后错误代码的条件,因此根据您引用的文档,它实际上不应该这样做。此外,分配内存可能会通过HeapAlloc进行,它明确声明不调用SetLastError。 - Stefan
@Stefan HeapAlloc 的文档中并没有说到这一点。你只能得出“应用程序无法调用GetLastError来获取扩展错误信息”的结论。 - David Heffernan
@Stefan 我同意,关于GetLastError的设置文档不是很好。不过,文档确实考虑到了这一点(第二个引用)。你不应该依赖于它被记录在文档中,而应该在需要时立即调用GetLastError,因为任何事情都可能设置它。 - lcs
@Stefan:“但是在我所知道的std::string文档中,它没有记录设置最后错误代码的条件。” - 这是因为C++标准库从不记录实现细节。也不需要这样做,因为GetLastError的文档已经非常清晰明了: “当函数的返回值指示此类调用将返回有用数据时,应立即调用GetLastError函数。” - IInspectable

0

这是正常的 - “最后的错误”可以通过任何函数调用间接设置。
一些函数在成功时将其设置为“无错误”,因此如果您想可靠地使用它,需要在执行其他操作之前立即存储它。

如果您曾经遇到过“发生严重错误:操作成功完成”对话框,这很可能是原因。


不要任何功能。只有记录此行为的函数。请参考Ics的答案。 - Stefan
2
文档通常在错误代码方面存在错误。仅仅因为一个函数没有记录它设置了某个错误代码,并不意味着它不会这样做。 - David Heffernan
微软表示:“如果未记录函数设置最后一个错误代码,则此函数返回的值仅为最近设置的最后一个错误代码”,这意味着(至少对我来说)它可以通过另一个函数间接设置。 - molbdnilo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接