Home > Back-end >  C , idiom for returning a process result and result
C , idiom for returning a process result and result

Time:12-25

Currently I have something like this

class ProcessRes {
public:
    ProcessResult(PROCESS_RC rc, std::string msg, PubConf pub) :
        rc(rc),
        msg(msg),
        pub(pub)
    {}
    const PROCESS_RC rc;
    //valid when rc == PROCESS_RES::OK
    const std::string msg; 
    const PubConf pub;
};

And a processor class with a function that returns this, as:

ProcessResult process_msg(const RawMsg &msg);

So users uses this as:

ProcessResult res = processor.process_msg(m_raw);
if(res.rc == PROESS_RC::OK) {
  //use res.msg/res.pub
} else { //deal with errors

}

The processor_msg() does internally e.g. this on errors:

 if (!match) {
      return ProcessResult(PROCESS_RC::NO_MATCH, "", PublishConf{});
 }

This doesn't quite feel right. Is there a common idiom for returning a result or an error code if the result isn't available ?

  • I'm not too happy about using std::optional, as it doesn't convey the result code
  • the c 20 std::expected sounds like it solves this, but I can't use c 20 yet.

CodePudding user response:

There is no C authority of some kind that maintains a list of officially accepted "idioms", so you can do whatever makes sense to you. However, there are distinct advantages to using std::variant in this use case:

typedef std::variant<PROCESS_RC, std::tuple<std::string, PubConf>> msg_result_t;

Your process_msg returns the variant with a PROCESS_RC value only in case of an error; or a tuple with the msg and the PubConf object. If you wish you can still have a ProcessRes variant value, instead with the two fields. It now becomes a simple aggregate object.

This has the benefit of using core C language features to force you to explicitly add proper error handling. You will have no other choice but to explicitly check whether each message result was successful, or not, in some form or fashion, before working with the results. std::visit will be the convenient means of doing that:

struct handle_result {

    void operator()(PROCESS_RC rc)
    {
        // Deal with the error
    }

    void operator()(const std::tuple<std::string, PubConf> &res)
    {
        const auto &[msg, pub] = res;

        // ... Deal with the results
    }
};

std::visit(handle_result{}, res);

Or you can still use std::holds_alternative. Or, the overloaded-based approach from cppreference.com, to make your C compiler do all the hard work of constructing this kind of a class from convenient lambdas, on the fly, each time.

Even if you forego all of that, use std::get, and hope for the best, if something unfortunate happens the C library will do you the courtesy of throwing an exception.

CodePudding user response:

Error handling is a very opinionated topic. Thus no one way is preferred in the community as a whole. But within organizations, a certain style is chosen based which provides clarity, performance, and predictability.

  1. Global error handler - eg. GetErrNo(); Requires caller to call if they care about it. Thread-safety is difficult;
  2. error value return - eg. HRESULT DoFunc(int param, int &out); Requires out params.
  3. error structure - eg. std::expected, std::optional etc.. Better than 1 or 2, but involves more machinery.
  4. Exceptions - try catch Requires RTTI and heap allocation. Allows unrolling the stack many levels deep.

I would also ask yourself if these errors are recoverable or if you need to bail out to get to a stable state. Exceptions are useful for the latter.

Ultimately this is a question of who is responsible for the error handling, the caller, the callee, or somewhere higher on the call stack.

CodePudding user response:

According to the problem description, it seems you may be interested with "Railway-oriented programming" idiom that intends a chain of operations. Each operation has to return success/failure state and resulting data. Thus, each operation can switch you to "green rail" (operation successful) or "red rail" (error detected). According to desired logic, you can introduce ability to cancel the execution (i.e. stay on red rail if error) or you can try to recover from the error (switching from red to green via a retry, or fallback policy, or etc).

The idiom is language-agnostic so you can use it on C#, F#, Kotlin, Python etc. whatever your choice is. https://fsharpforfunandprofit.com/rop/

CodePudding user response:

The solution here is a matter of design choice, considering that you are dealing with processes then you expect that they might fail. But how you deal with failures it should be your (team) design choice.

If you are writing some intermediary layer maybe you just need to forward the return code to higher layers then you have to send the return code further in whatever structure you see fit. If you have something more than a return code (an error string for example) I would recommend to pack it in a single response struct/object,

Otherwise, if you deal with the errors at this level, then it doesn't matter what you return in case of failure. And in this case an optional might be suited.

The throw option can be useful if some higher level is going to catch it, but I would consider this as a last choice.

When I am in doubt about which direction to go, I always choose the option that simplifies the "consumer" or "calling" party. If you could allow the caller to drop a single if statement then it's a win.

  •  Tags:  
  • c
  • Related