In this post I would like to discuss two issues brought up by the readers regarding previous posts on using
- Storing 0 in error codes, and using non-0 success values.
- Concerns about using globals.
0 ⇔ Success?
In the other post I have mentioned that using numeric value 200 to indicate success (this is the case for HTTP status codes) is incorrect because it would make contextual conversion to
bool fail to meet the intuitive expectation:
if (std::error_code ec = fun()) handle_failure(); else handle_success();
It is not uncommon to see this encoding scheme: 0 represents success, non-0 values represent different kind of failures. One such example is reflected in what we return from function
main. However, there are systems that do not follow this principle. To name two examples:
- HTTP protocol status codes: any value between 100 an 399 actually represents success.
- Vulkan return codes: negative numbers represent failures, zero represents plain success, positive numbers represent success with additional status info, like “time-out occurred” (warnings).
Typically, the most relevant information we expect of a returned status code, independent of any system, is whether the operation has succeeded or failed. If it failed we have to abandon the subsequent operations that depend on the former. This construction of operation dependency is essential to any error handling system. We could create a category for HTTP error codes where comparing to value
http::success would take into account all values representing success from HTTP operations, but it would not be generic: we would need a different special condition for
vulkan::success, and so on. In a generic context, where we get just an
std::error_code from an unknown system there would be no way of asking, “did the operation succeed or fail?”
The contextual conversion to
bool could be a good candidate for such generic discrimination into success or failure, but it requires that we change the status value encoding for systems like HTTP or Vulkan. But this would compromise another design goal of
<system_error>: that it can store a numeric value from any system directly — without any remapping.
A different conclusion to mine has been drawn: maybe non-zero success codes are fine, but you have to accept that there is no generic way of determining success or failure from an
std::error_code. It is not that often that you need a generic check form a system library, anyway.
But one thing is clear: if you use non-zero success codes you should not rely on contextual conversion to
<system_error> requires using global objects (actually, namespace-scope objects) for identifying error categories. Globals are known not to work well with DLLs on Windows. If your program is linked with a static library L, and it loads a DLL, and this DLL was also linked with the same static library L, the code of L is duplicated along with the globals it defined.
Thus the program has one instance of global G, and the DLL has the second instance, with a different address. You think that you are accessing a single global, but this is not the case: depending on whether the call is made directly from the program or from a DLL, a global from a different address is used. The same problem applies also to function-local static objects and any kind of singletons: they become doubletons.
One could ask if this behavior is standard-compliant, but since the C++ Standard does not have to say anything about shared libraries, it has also nothing to say about how they would interact with globals.
Since error categories depend on the uniqueness of the address, they will not work correctly if you use them in this configuration. I did not observe the same problems with shared libraries on Linux, although one reader in this blog claimed to the contrary.
The only solution to this problem that I am aware of, when you are using any DLL in your program, is to make sure that every library that uses globals is also made a DLL itself (rather than being statically linked):
This problem is characteristic of any library that uses globals. For instance, Boost.Log gives the same advice (see here). Maybe the only difference is that from the nature of a library like Boost.Log it is immediately clear that it will be using some globals; whereas
error_codes are trivially-copyable value-semantic types, and unless you learn the implementation details you would have never expected that there are any globals involved.