Why is std::string constructor resetting GetLastEr

2020-03-25 03:23发布

I'm calling Windows APIs from C++ code and I have a helper method to do the FormatMessage stuff and throw an exception for error handling. The signature of the function is

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

I've noticed something strange. This code does not work properly:

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

The error code that is passed to throw_system_error is always zero.

Where as this works just fine:

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

Some more investigation showed that this version has the same problem:

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

It looks for all the world as if the constructor of std::string is resetting the error code.

My guess would be that std::string is allocating memory internally which causes some system call that then sets the last error back to zero.

Anyone knows what is actually going on here?

Visual C++ 2015, 64bit.

3条回答
Lonely孤独者°
2楼-- · 2020-03-25 03:46

Let's see the GetLastError documentation:

Most functions that set the thread's last-error code set it when they fail. However, some functions also set the last-error code when they succeed.

You should call the GetLastError function immediately when a function's return value indicates that such a call will return useful data. That is because some functions call SetLastError with a zero when they succeed, wiping out the error code set by the most recently failed function.

So there is one function calling SetLastError, very likely one that allocates memory: When you construct a string, new is called to allocate memory.

Now, let's see new's implementation in vc++. There is a very good answer to that question in Stack Overflow : https://softwareengineering.stackexchange.com/a/293209

It depends if you are in debug or release mode. In release mode, there is HeapAlloc/HeapFree which are kernel functions,

while in debug mode (with visual studio) there is a hand written version of free and malloc (to which new/delete are re-directed) with thread locks and more exceptions detection, so that you can detect more easily when you did some mistakes with you heap pointers when running your code in debug mode.

So in release mode, the function called is HeapAlloc, which does NOT call SetLastError. From the documentation:

If the function fails, it does not call SetLastError

So the code should work properly in release mode. However, in the debug implementation, the function FlsGetValue is called, and that function calls SetLastError when succeeded.

It's very easy to check this,

#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;
}

It outputs the following:

23
0

So FlsGetValue has called SetLastError(). To prove that it is called only on debug we can do the following test:

#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;
}

If you run it in debug mode, it will give you, because it calls FlsGetValue:

23
0

However, if you run it in release mode, it produces, because it calls HeapAlloc:

23
23
查看更多
看我几分像从前
3楼-- · 2020-03-25 03:51

Per the documentation for GetLastError

The Return Value section of the documentation for each function that sets the last-error code notes the conditions under which the function sets the last-error code. Most functions that set the thread's last-error code set it when they fail. However, some functions also set the last-error code when they succeed. If the function is not documented to set the last-error code, the value returned by this function is simply the most recent last-error code to have been set; some functions set the last-error code to 0 on success and others do not.

At some point during the construction of std::string, SetLastError is called. The standard library on windows uses Win32 calls as part of its implementation.

Your second method (that works) is the correct way to use GetLastError

You should call the GetLastError function immediately when a function's return value indicates that such a call will return useful data. That is because some functions call SetLastError with a zero when they succeed, wiping out the error code set by the most recently failed function.

查看更多
Rolldiameter
4楼-- · 2020-03-25 04:01

This is normal - the "last error" can be set indirectly through any function call.
Some functions set it to "no error" on success, so if you want to use it reliably you need to store it immediately before you do anything else.

If you've ever encountered an "A serious error occurred: The operation completed successfully" dialogue, this is probably the reason.

查看更多
登录 后发表回答