Skip to content

Using ioresult and detecting errors

Frank Pagliughi edited this page Apr 24, 2023 · 5 revisions

In v0.9 of sockpp, the library started moving toward more portable, thread-safe, error handling. The generic result<T> template introduced a function return type that can definitively be a successful return value of type T, or an error of type std::error_code.

The standard error codes help bring uniformity across platforms, particularly in regard to Windows using different error codes than the rest of the world. The "portable error conditions can be found here, with POSIX equivalents listed.

The result<T> type is loosely inspired by Rust's Result, and is similar to the standard C++17 variant or option. The result can either be a success or an error, and once determined, the success result or error value can be extracted.

At a minimum, the value resolves to a bool, so can be used to determine the failure of an operation:

if (!sock.read_r(buf, BUF_SZ)) {
  cerr << "Read failed" << endl;
}

But more likely, the success or error value would be wanted for subsequent operations or for reporting to the user:

auto res = sock.read_r(buf, BUF_SZ);
if (res) {
    cout << "Read " << res << " bytes" << endl;
}
else {
    cerr << "Error: " << res << endl;
}

The ostream inserter '<<' for result detects whether the value is a success or error and prints it appropriately.

The success value type, T can be any type that has a default constructor, copy semantics, and an ostream inserter, and anything that make sense to be returned from a function would be appropriate, except that is should not be a std::error_code as that would make it difficult to distinguish between success and return value.

Internally, the result contains both a success T value and a std::error_code. The error code is always valid, and determines if the result indicates a success or failure. A zero error code indicates the result was a success and the T value contains the successful return value of the operation. On a failure, the success value will be equal to the default T value,T{}. This is all internal to the object and should be considered implementation details that may change in the future.

In the sockpp library, the result is typically used for I/O operations that return an int value where an error is indicated by a value less-than zero, and a success by an amount greater-than zero. As such, a specialization of the result type ioresult is used by the library, defined as:

using ioresult = result<int>;

For this, the low-level error integer "last error" is converted into a std::error_code whereas a value greater than zero is saved as the success return.

Since failure values are definitive, the result can be compared directly against values of type std::error_code or, more likely, the portable error conditions represented by std::errc. For example we could do this:

while (true) {
    auto ret = sock.read_r(buf, BUF_LEN);
    if (ret == errc::interrupted)  // If interrupted, try again
        continue;
    if (!ret)                      // Other errors, abort
        return ret;
    // Handle successful read
}

You should always check for a successful return before getting or using the success value(). Although a non-default value is definitive of a successful return, there could be a valid value containing the default.

Therefore, it should be used something like:

auto ret = sock.read_r(buf, BUF_LEN);
if (ret) {
    int n = ret.value();    // 'n' guaranteed to be >= 0
    process_buffer(buf, n);
}
Clone this wiki locally