Skip to main content
Post Closed as "Needs details or clarity" by Ben Cottrell, gnat, Bart van Ingen Schenau
made it more like a question
Source Link
Abyx
  • 1.4k
  • 4
  • 14
  • 20

It might be possible to add a new API, which would return another type, such as a better error code type (std::error_code), a monad-ish type (std::excepted), etc.
(Sure, monad-ish types are not error codes, but there is a catch, see below.)

So the question is how to upgrade such legacy C++ project to have better error types/codes.

Possible solutions

boost::error_code keeps std::source_location. This appears to be an overkill, and bloats size of the error type. Does it still fit into CPU registers?

std::error_code is a pair of an error code (int) and a pointer to an abstract std::error_category. It is possible to bind a concrete error category to a scoped enum type, so that resulting code would look like this:

enum class SomeErr { OK = 0, invalid_arg = EINVAL|0x10000, empty_out = EINVAL|0x20000}; class SomeErrCategory : std::error_category { ... }; std::error_code NewCls::foo(A arg, O& out) { if (!validate(arg)) return SomeErr::invalid_arg; if (!out) return SomeErr::empty_out; return SomeErr::OK; // same as std::error_code{SomeErr::OK, SomeErrCategory::instance} } 

Returning int and a pointer could use the rax:rdx registers, that's one more register than returning an int.

Boost.Outcome notes that error categories often use magic statics, which use a hidden lock guard check, thus constructing std::error_code might be longer than one might expect ("cmp imm, mem" + "jxx" instructions). Well, don't use magic statics, use constinit (note that current gcc/clang don't optimize it well).

Converting std::error_code ec to a negative errno code is tricky. If we know that the category is the SomeErrCategory from above - then return -(ec.value() & 0xffff);. However checking the type of an abstract class - that's a dynamic_cast<CommonCls*> or many comparisons with known error categories.

It's important to use a popular error type such as std::error_code in APIs, so that users that use different libraries wouldn't have to convert different error types.
However internally we can What should I use whatever fits the task.

The project has few thousands of return -Exxx;, so it's entirely possible to have one huge enum for error codes, and a single error category. The idea is the same as with the APIs - single type means no converting code.

Then why do we need the error category and std::error_code internally?
Returning enum type is almost the same as returning std::error_code.

auto foo() { return SomeErr::invalid_arg; } // instead of std::error_code foo() { return SomeErr::invalid_arg; } 

The benefit of std::error_code is that support different error categories. However, in this project, every function returns an int with negative errno code.

When rewriting the project, our new code has to call the old code, so we'd have to convert int to the enum.

constexpr int make_err(int ext_code, int errno_code) { return ext_code * 0x10000 + errno_code; } enum class SomeErr : int { OK = 0, legacy_EINVAL = make_err(0, EINVAL), // ... legacy errno codes up to 0x10000 // New error codes: invalid_arg = make_err(1, EINVAL), empty_out = make_err(2, EINVAL), }; int to_int_errno(SomeErr err) { return -(int(err) & 0xffff); } SomeErr from_int_errno(int e) { return {-e}; } 

Given that the actual amount of errno codes is not that large, we can pack legacy errno codes, and new more specific error codes into a single enum value.

int legacy_func(); // returns errno code SomeErr new_func() { SomeErr err = from_int_errno(legacy_func()); ... if (to_int_errno(err)) == -EINVAL) {...} // legacy error handling } 

As for the monad-ish types like std::expected, most of the functions return expected<void, error_type>, while others (e.g. read/write) return expected<ssize_t, error_type>.

In a sense, int with a negative errno code and a non-negative non-error value is a packed expected<ssize_t, errno_int_t>.
Same works for SomeErr - it might also keep ssize_t, in theory, however it'd much better to use a different type, e.g.?

union SizeOrErr { int neg_sz; SomeErr err; /* monad-ish interface functions */ }; 

It might be possible to add a new API, which would return another type, such as a better error code type (std::error_code), a monad-ish type (std::excepted), etc.
(Sure, monad-ish types are not error codes, but there is a catch, see below.)

So the question is how to upgrade such legacy C++ project to have better error types/codes.

Possible solutions

boost::error_code keeps std::source_location. This appears to be an overkill, and bloats size of the error type. Does it still fit into CPU registers?

std::error_code is a pair of an error code (int) and a pointer to an abstract std::error_category. It is possible to bind a concrete error category to a scoped enum type, so that resulting code would look like this:

enum class SomeErr { OK = 0, invalid_arg = EINVAL|0x10000, empty_out = EINVAL|0x20000}; class SomeErrCategory : std::error_category { ... }; std::error_code NewCls::foo(A arg, O& out) { if (!validate(arg)) return SomeErr::invalid_arg; if (!out) return SomeErr::empty_out; return SomeErr::OK; // same as std::error_code{SomeErr::OK, SomeErrCategory::instance} } 

Returning int and a pointer could use the rax:rdx registers, that's one more register than returning an int.

Boost.Outcome notes that error categories often use magic statics, which use a hidden lock guard check, thus constructing std::error_code might be longer than one might expect ("cmp imm, mem" + "jxx" instructions). Well, don't use magic statics, use constinit (note that current gcc/clang don't optimize it well).

Converting std::error_code ec to a negative errno code is tricky. If we know that the category is the SomeErrCategory from above - then return -(ec.value() & 0xffff);. However checking the type of an abstract class - that's a dynamic_cast<CommonCls*> or many comparisons with known error categories.

It's important to use a popular error type such as std::error_code in APIs, so that users that use different libraries wouldn't have to convert different error types.
However internally we can use whatever fits the task.

The project has few thousands of return -Exxx;, so it's entirely possible to have one huge enum for error codes, and a single error category. The idea is the same as with the APIs - single type means no converting code.

Then why do we need the error category and std::error_code internally?
Returning enum type is almost the same as returning std::error_code.

auto foo() { return SomeErr::invalid_arg; } // instead of std::error_code foo() { return SomeErr::invalid_arg; } 

The benefit of std::error_code is that support different error categories. However, in this project, every function returns an int with negative errno code.

When rewriting the project, our new code has to call the old code, so we'd have to convert int to the enum.

constexpr int make_err(int ext_code, int errno_code) { return ext_code * 0x10000 + errno_code; } enum class SomeErr : int { OK = 0, legacy_EINVAL = make_err(0, EINVAL), // ... legacy errno codes up to 0x10000 // New error codes: invalid_arg = make_err(1, EINVAL), empty_out = make_err(2, EINVAL), }; int to_int_errno(SomeErr err) { return -(int(err) & 0xffff); } SomeErr from_int_errno(int e) { return {-e}; } 

Given that the actual amount of errno codes is not that large, we can pack legacy errno codes, and new more specific error codes into a single enum value.

int legacy_func(); // returns errno code SomeErr new_func() { SomeErr err = from_int_errno(legacy_func()); ... if (to_int_errno(err)) == -EINVAL) {...} // legacy error handling } 

As for the monad-ish types like std::expected, most of the functions return expected<void, error_type>, while others (e.g. read/write) return expected<ssize_t, error_type>.

In a sense, int with a negative errno code and a non-negative non-error value is a packed expected<ssize_t, errno_int_t>.
Same works for SomeErr - it might also keep ssize_t, in theory, however it'd much better to use a different type, e.g.

union SizeOrErr { int neg_sz; SomeErr err; /* monad-ish interface functions */ }; 

It might be possible to add a new API, which would return another type, such as a better error code type (std::error_code), a monad-ish type (std::excepted), etc.

So the question is how to upgrade such legacy C++ project to have better error types/codes? What should I use for that?

Source Link
Abyx
  • 1.4k
  • 4
  • 14
  • 20

Error codes in a legacy C++ project

Background

I have a large C++ project which uses system error codes from errno.h, in C style.

int Cls::foo(A arg, O* out) { if (!validate(arg)) return -EINVAL; if (!out) return -EINVAL; return 0; } 

The code is not exception safe, and it's not covered by tests. (And this question is not about tests nor how to magically turn legacy code into non-legacy)

Existing functions of user-facing libraries (API) have to preserve their int return types, and the same values of error codes, for backward compatibility.

Some functions are performance-critical, so a refactoring shouldn't decrease their performance.

Toolchain supports C++20.

The problem

The errno codes aren't very helpful when it comes to error handling and manual debugging. E.g. half of return -Exxx; statements are EINVAL. Often it's not clear what actually happened.

It might be possible to add a new API, which would return another type, such as a better error code type (std::error_code), a monad-ish type (std::excepted), etc.
(Sure, monad-ish types are not error codes, but there is a catch, see below.)

int Cls::foo(A arg, O* out) { return to_int_errno(NewCls::foo(arg, out)); } 

So the question is how to upgrade such legacy C++ project to have better error types/codes.

Possible solutions

boost::error_code keeps std::source_location. This appears to be an overkill, and bloats size of the error type. Does it still fit into CPU registers?

std::error_code is a pair of an error code (int) and a pointer to an abstract std::error_category. It is possible to bind a concrete error category to a scoped enum type, so that resulting code would look like this:

enum class SomeErr { OK = 0, invalid_arg = EINVAL|0x10000, empty_out = EINVAL|0x20000}; class SomeErrCategory : std::error_category { ... }; std::error_code NewCls::foo(A arg, O& out) { if (!validate(arg)) return SomeErr::invalid_arg; if (!out) return SomeErr::empty_out; return SomeErr::OK; // same as std::error_code{SomeErr::OK, SomeErrCategory::instance} } 

Returning int and a pointer could use the rax:rdx registers, that's one more register than returning an int.

Boost.Outcome notes that error categories often use magic statics, which use a hidden lock guard check, thus constructing std::error_code might be longer than one might expect ("cmp imm, mem" + "jxx" instructions). Well, don't use magic statics, use constinit (note that current gcc/clang don't optimize it well).

Converting std::error_code ec to a negative errno code is tricky. If we know that the category is the SomeErrCategory from above - then return -(ec.value() & 0xffff);. However checking the type of an abstract class - that's a dynamic_cast<CommonCls*> or many comparisons with known error categories.

It's important to use a popular error type such as std::error_code in APIs, so that users that use different libraries wouldn't have to convert different error types.
However internally we can use whatever fits the task.

The project has few thousands of return -Exxx;, so it's entirely possible to have one huge enum for error codes, and a single error category. The idea is the same as with the APIs - single type means no converting code.

Then why do we need the error category and std::error_code internally?
Returning enum type is almost the same as returning std::error_code.

auto foo() { return SomeErr::invalid_arg; } // instead of std::error_code foo() { return SomeErr::invalid_arg; } 

The benefit of std::error_code is that support different error categories. However, in this project, every function returns an int with negative errno code.

When rewriting the project, our new code has to call the old code, so we'd have to convert int to the enum.

constexpr int make_err(int ext_code, int errno_code) { return ext_code * 0x10000 + errno_code; } enum class SomeErr : int { OK = 0, legacy_EINVAL = make_err(0, EINVAL), // ... legacy errno codes up to 0x10000 // New error codes: invalid_arg = make_err(1, EINVAL), empty_out = make_err(2, EINVAL), }; int to_int_errno(SomeErr err) { return -(int(err) & 0xffff); } SomeErr from_int_errno(int e) { return {-e}; } 

Given that the actual amount of errno codes is not that large, we can pack legacy errno codes, and new more specific error codes into a single enum value.

int legacy_func(); // returns errno code SomeErr new_func() { SomeErr err = from_int_errno(legacy_func()); ... if (to_int_errno(err)) == -EINVAL) {...} // legacy error handling } 

As for the monad-ish types like std::expected, most of the functions return expected<void, error_type>, while others (e.g. read/write) return expected<ssize_t, error_type>.

In a sense, int with a negative errno code and a non-negative non-error value is a packed expected<ssize_t, errno_int_t>.
Same works for SomeErr - it might also keep ssize_t, in theory, however it'd much better to use a different type, e.g.

union SizeOrErr { int neg_sz; SomeErr err; /* monad-ish interface functions */ };