0

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.

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? What should I use for that?

4
  • @DocBrown, is it better now? Commented Apr 1, 2024 at 8:30
  • A little bit. Still it is unclear what kind of help you need. It seems you already have plenty ideas of how the new API could look like. Now it is up to you to make a cost/benefit analysis, something which we cannot do for you. Commented Apr 1, 2024 at 21:32
  • I need an expert opinion on how to modernize a C++ project. The question is not about API, it's about a type that should be used internally. It's we have one Either-like type int, we want to replace it with another, we cannot use std::optional<std::error> because there is no std::error (yet?), what should it be? Really, a question like "what to use instead of const char*" doesn't require a const/benefit analysis - use owning std::string or non-owning std::string_view. What's wrong with the question "what to use instead of int aka either<size_t, errc>"? Commented Apr 2, 2024 at 16:22
  • There are plenty of options to implement error handling differently, but a sensible transition of what you have now to something different depends 100% on how the code is used currently, how much resources you are willing to invest to restructure the code, how much time you can invest, or in short, on tons of context information. Often, in such a situation the best redommendation we can give you is indeed "try to improve the code a little bit without investing too much and see how it works, then rethink if that is what you want". Commented Apr 2, 2024 at 17:17

1 Answer 1

2

First of all: why you reuse system error codes? There's even more weirdness in your code with - sign.

So the first step to improve your situation is to introduce your own error codes:

const int INVAILD_ARG = 1; const int NULL_OUT = 2; int Cls::foo(A arg, O* out) { if (!validate(arg)) return INVALID_ARG; if (!out) return NULL_OUT; return 0; } 

This already solves your "EINVAL everywhere" issue. Be more specific, return more descriptive error codes.

Then we have another issue: how is the user supposed to know what error/result codes the function can return? Old school way is to write documentation, but now we have better tools. Turn those error codes into enum and return an enum. In C++ that would be enum class for type safety.

As a minor but also important thing: rename "error code" into "result code" or just "result". I mean 0 is also a valid code, no?

Finally, maybe result code alone is not enough, and you actually want to return more data. Like description or something. That is especially true for "ok" result code. Rust would be very good here, because it allows attaching arbitrary data to enums. C++ unfortunately doesn't have such feature out of the box, and so it has to be emulated. This can be done with unions for example. Something like this:

public enum class ClsFooCodes { Ok = 0, InvalidArg = 1, NullOut = 2, } public class ClsFooResult { public: ClsFooCodes code; const std::string& get_invalid_arg_message() const { if (code != ClsFooCodes::InvalidArg) throw std::runtime_error("wrong code"); return invalid_arg_message; } // ... and so on private: union { std::string invalid_arg_message; std::string null_out_message; // Put here result code specific objects } } 

And then your API returns const ClsFooResult. The caller can check result by doing appropriate switch and calling get_ kind of methods.

WARNING: my C++ might be a bit rusty, don't treat the code above as literall C++, it might fail to compile.

Of course this is a rough sketch, I didn't put mandatory constructors and destructors (especially destructor has to destroy internal data based on code). And it will be quite a lot of boilerplate if you were to define such class on every api endpoint. But then again I'm pretty sure you can avoid most of that boilerplate with some template magic.

It might even be possible to do something like Rust, where you have a single match method where you pass enum values and corresponding handlers (which accept the additional data as args). That way you will avoid the if check in my get_ methods. But I can't give you concrete solution unfortunately. You might want to do some research on this.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.