fpagliughi / sockpp

Modern C++ socket library.
BSD 3-Clause "New" or "Revised" License
769 stars 126 forks source link

Moving toward stateless I/O without exceptions #77

Open fpagliughi opened 1 year ago

fpagliughi commented 1 year ago

Commit 784dc0958c08f4894258c8a2f5b411e36b38fad4 in PR #17 introduced stateless I/O functions, like read_r() and writre_r(), which return an ioresult type that wrap the success and specific error values derived from errno or GetLastError().

On an unrelated note, in Issue #72, @JakeSays suggested getting rid of exceptions in the few places they're used, and @ldgeng, suggested using std::error_code.

I was not even aware std::error_code existed, but it looks very appropriate for this library. It would be a good way to eliminate platform-specific differences of checking errors on Win32 that has been plaguing us already. And it would present a road forward for handling errors when TLS libraries are introduced in a future version of sockpp.

So, taken all together, I'm thinking of doing this...

  1. Convert the "last error" in the library to return an error code, like:
    std::error_code socket::get_last_error();
  2. Make a generic result<T> type where T is the success variant and error_code is the error.
    template <typename T>
    class result {
        /** The return value of an operation, if successful */
        T val_;
        /** The error returned from an operation, if failed */
        error_code err_;
    // ...
    };
  3. An ioresult would be a specialization of result<int>:
    using ioresul = result<int>;
  4. Remove exceptions and return a result<T> in their place. The whole API will be noexcept.
  5. Constructors that would have thrown now just set the last error, and thus would need to be checked (similar to iostreams, I guess).
  6. Phase out the need for maintaining the error state. In the next major release, move to all I/O operations being stateless, returning an ioresult. The only explicit need for the state would be checking the object after construction.

The last point would allow a single socket to be thread-safe for two threads where one reads from the socket and the other writes to it. This is a common pattern in socket apps in C, but couldn't be done in sockpp since get_last_error() is not thread safe.

fpagliughi commented 1 year ago

After going through some of this, it appears that it would be difficult to provide the same usability and level of detail that some exceptions provide if we get rid of them. So, it seems that making exceptions a build option for the library might be a more appropriate way to handle this... at least for now. As the result<> type creeps in and evolves, this may change, making exceptions less useful.

So, for now, exceptions will be included at build time by default, but can be turned off with the CMake build option, SOCKPP_WITH_EXCEPTIONS, like:

$ cmake -DSOCKPP_WITH_EXCEPTIONS=OFF ..
fpagliughi commented 7 months ago

Here's how I think things will play out...

  1. The std::error_code and result<T> will be introduced in v0.9, but will only be used in a limited fashion, where useful.
  2. Some functions that had to throw previously to distinguish error types - like GAI errors instead of errno will now return a result<T> instead of throwing. These will be marked noexcept.
  3. In the few cases where exceptions are still useful, particularly in constructors, those will remain, but the classes will also provide comparable methods that take an error_code& parameter to return any failure state if encountered. So the application can choose to completely avoid exceptions if desired.
  4. A full conversion to stateless functions will likely happen in the following release, v0.10. This will eliminate the need for get_last_error() and make sockets somewhat more thread friendly by removing the cached error state.