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.
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>"?