Tuesday, September 7, 2010

GetLastError and side effects

Представим себе, что у нас в проекте есть такое чудо:
hSomething = ::CreateSomething(...);
if (!hSomething)
throw std::runtime_error("Cannot create something: " + some_utils::FormatError(GetLastError())); *
_Winnie C++ Colorizer
Какие проблемы могут быть с этим кодом?

Ну, о том, что вообще-то имеет смысл сообщать об ошибке в виде кода ошибки, а не в виде невнятной строки (непонятно на каком языке) речь даже не идет. Представим, что нам очень нужно записать куда-нибудь именно строку, т.е. на месте throw std::runtime_error вполне мог бы быть некий LogLog.

Возвращаемся к коду. Если бы я увидел такой код на review, я посчитал бы его некорректным, аргументируя тем, что компилятор вполне может расставить вызовы функций в следующей последовательности:
call std::basic_string<char>::std::basic_string<char>(const char *)
....
call GetLastError
call some_utils::FormatError
call _CxxThrowException
_Winnie C++ Colorizer


Не трудно представить, к чему это приведет, учитывая, что конструктор строки вполне может вызвать malloc, а значит и HeapAlloc. (Который пренепременно вызовет SetLastError(NO_ERROR) в случае успешного выделения памяти)

Поэтому, я сам всегда старался писать так, чтобы исключить возможные side effects:
hSomething = ::CreateSomething(...);
if (!hSomething)
{
DWORD dwError = GetLastError();
throw std::runtime_error("Cannot create something: " + some_utils::FormatError(dwError));
}
_Winnie C++ Colorizer

....
До тех пор, пока, совершенно случайно, не наткнулся на тот факт, что утверждение
... HeapAlloc. Который пренепременно вызовет SetLastError(NO_ERROR) в случае успешного выделения памяти.
не является истинным.

MSDN утверждает, что HeapAlloc вообще никогда не зовет SetLastError, а HeapReAlloc зовет ее только в случае неудачного выделения памяти. Тестами подтверждается.

Да и вообще, как выяснилось, количество API функций, которые устанавливают SetLastError(NO_ERROR) по поводу и без повода не так уж и велико: сюда входит файловое API, функции работы с ini файлами, с ресурсами, GlobalUnlock/LocalUnlock, и, почему-то, EqualSid(???). Все.

Поэтому, перезатереть значение NtCurrentTeb()->LastErrorValue в нашем случае - некому. Минус одна старая параноидальная привычка (когда-то я таки обжегся на таких сайд эффектах, но код уже и не припомню).

1 comment:

ligen said...

https://connect.microsoft.com/VisualStudio/feedback/details/1775690/calling-operator-new-may-set-lasterror-to-0