How to “exit current function, if error on current statement” in C++

谁说我不能喝 提交于 2019-12-24 22:29:11

问题


I start with an example to elaborate my problem. And conclude with the exact statement of the question at the end.

So in C, we can write a macro like this,

#define NO_ERROR 0
#define RETURN_IF_ERROR(function)                                       \
{                                                                       \
  RetCode = function;                                                   \
  if (RetCode != NO_ERROR)                                              \
  {                                                                     \
      LOG(ERROR,"[%d] line [%d] [%s]", RetCode, __LINE__, __FILE__ ) ); \
      return RetCode;                                                   \
  }                                                                     \
  else {                                                                \
      LOG(VERBOSE,"[%d] line [%d] [%s]", RetCode, __LINE__, __FILE__ ) );\
  }                                                                     \
}

Now this macro could be used in a function like this,

int ConstructAxes() {
    RETURN_IF_ERROR(GetAxis("alpha"));
    RETURN_IF_ERROR(GetAxis("beta"));
    RETURN_IF_ERROR(GetAxis("gamma"));
    RETURN_IF_ERROR(GetAxis("delta"));
    .
    .
    .
}

So, we exit the current function (e.g. ConstructAxes) immediately with an error code, if one of the functions called within, returns with an error. Without that macro, for each of those 4 statements, I would have to write an if...else block. I'd end up with 4 lines of code that shows the actual functionality or task and then another 16 lines of error-checking (and/or optional logging). (I have seen functions which are 700+ lines long with only 20~30 lines of functionality and 600+ lines of if...else error checking and logging. Not very easy to follow the main logic then.)

(p.s. before anyone points out, I cannot use exceptions. This is a legacy project and exceptions are not used and not desired to be used, nor am I an expert at writing exception-safe code. Also before anyone points out, the returned error code is reinterpreted into some meaningful text, at a higher level. Where and how is not relevant to this question at hand.)

The question is, macros can be problematic and I'd prefer a function. So is there some clean&elegant way to do this in C++ without using a macro? I have a feeling it is NOT possible.


回答1:


You might write a loop, something like:

int RunFunctions(int line, // __LINE__
                 const char* file, // __FILE__
                 std::initializer_list<std::function<int()>> functions)
{
    int counter = 0;
    for (auto f : functions) {
        auto RetCode = f();

        if (RetCode != NO_ERROR) {
            LOG(ERROR,"[%d] line [%d] [%s] [%d]", RetCode, line, file, counter ));
            return RetCode;
        } else {
            LOG(VERBOSE,"[%d] line [%d] [%s] [%d]", RetCode, line, file, counter ) );
        }
        ++counter;
    }
    return NO_ERROR;
}

int ConstructAxes() {
    return RunFunctions(__LINE__, __FILE__, {
        []() { return GetAxis("alpha"); },
        []() { return GetAxis("beta"); },
        []() { return GetAxis("gamma"); },
        []() { return GetAxis("delta"); }
    });
}

Until we have std::source_location or similar, __LINE__, __FILE__ would be required.

If you don't need capture, you may change std::function by function pointers.




回答2:


Ok, for an embedded device you may need to avoid any complex C++ goodies that could require memory allocation. But I would split the logging part which must always occur and the block short exit.

  int test_and_log(int RetCode) {
    if (RetCode != NO_ERROR)
      {
          LOG(ERROR,"[%d] line [%d] [%s]", RetCode, __LINE__, __FILE__ ) );
      }
      else {
          LOG(VERBOSE,"[%d] line [%d] [%s]", RetCode, __LINE__, __FILE__ ) );
      }
    return RetCode;
  }

Then a minimalist macro (the C way):

#define RETURN_IF_ERROR(x) { int cr; if ((cr = test_and_log(x)) != NO_ERROR) return cr; }

int ConstructAxes() {
    RETURN_IF_ERROR(GetAxis("beta"));
    RETURN_IF_ERROR(GetAxis("gamma"));
    RETURN_IF_ERROR(GetAxis("delta"));
    .
    .
    .
    return 0;                            // ensure a return value if every line passes
}

But for C++ I would still use minimalist exception handling by throwing a int value:

  void test_and_log(int RetCode) {
    if (RetCode != NO_ERROR)
      {
          LOG(ERROR,"[%d] line [%d] [%s]", RetCode, __LINE__, __FILE__ ) );
          throw RetCode;
      }
      else {
          LOG(VERBOSE,"[%d] line [%d] [%s]", RetCode, __LINE__, __FILE__ ) );
      }
  }

and then:

int ConstructAxes() {
  try {
    test_and_log(GetAxis("beta"));
    test_and_log(GetAxis("gamma"));
    test_and_log(GetAxis("delta"));
    .
    .
    .
  }
  catch (int RetCode) {
    return RetCode;
  }
  return 0;                            // ensure a return value if every line passes
}

This is rather hacky, because best practices recommend to only throw subclasses of std::exception to have consistent exception handling. But as you say that you do not want to use exceptions in your application it could be acceptable. The nice point on an embedded system is that no plain exception object is ever constructed. But please never use it in normal code.


And if you want to exchange memory for processing time, you can always declare test_and_log with an inline specifier...




回答3:


While you cannot directly do what you want, you can get close :

int ConstructAxes() {
    int retCode = NO_ERROR;

    auto fail_if_error = [&retCode](int result) -> bool {
        retCode = result;
        if (retCode != NO_ERROR) {
            LOG(ERROR, "[%d] line [%d] [%s]", retCode, __LINE__, __FILE__);
            return false;
        }
        LOG(VERBOSE, "[%d] line [%d] [%s]", retCode, __LINE__, __FILE__);
        return true;
    };

    fail_if_error(GetAxis("alpha"))
      && fail_if_error(GetAxis("beta"))
      && fail_if_error(GetAxis("gamma"))
      && fail_if_error(GetAxis("delta"));

    return retCode;
}



回答4:


Since you seem to be looking for more of a C solution, the only improvement I would suggest over what you have right now would be to have one point of error handling in the function (suggested by Felix in the comments) that would then also perform cleanup if needed.

#define RETURN_IF_ERROR(function)                                       \
{                                                                       \
  RetCode = function;                                                   \
  if (RetCode != NO_ERROR)                                              \
  {                                                                     \
      goto return_with_error;                                                   \
  }                                                                     \
  else {                                                                \
      LOG(VERBOSE,"[%d] line [%d] [%s]", RetCode, __LINE__, __FILE__ ) );\
  }                                                                     \
}

int ConstructAxes() {
    int RetCode;
    RETURN_IF_ERROR(GetAxis("alpha"));
    RETURN_IF_ERROR(GetAxis("beta"));
    RETURN_IF_ERROR(GetAxis("gamma"));
    RETURN_IF_ERROR(GetAxis("delta"));
    .
    .
    .
    return RetCode;
return_with_error:
    cleanup();
    LOG(ERROR,"[%d] line [%d] [%s]", RetCode, __LINE__, __FILE__ ) );
    return RetCode;
}

Using goto for error handling and cleanup in C is fine.




回答5:


Talking C:

Your approach is OK, and marcos are only an issue if not used correctly.

So, I'd go for the well structured do {... break on error ...} while(0) approach. This also helps you sticking to the pattern, that a function shall have only one entry- and one exit-point.

Also for the sake of debugging and ease of reading, I'd move the "jump"-statement (break here) out of the macro.

#define NO_ERROR (0)

// log level IDs
#define LL_ERROR (0)
// more log levels here
#define LL_VERBOSE (10)
// more log levels here
#define LL_DEBUG (13)
// more log levels here
#define LL_MAXIMUM (16)

// log level long names
static const char * log_level_names[LL_MAXIMUM] = {
  "error",
  // more here
  "verbose"
  // more here
  "debug"
  // more here
}

int loglevel = LL_ERROR; // default logging-level to error; to be set to any LL_xxxx

// return date-time-stamp string (can be any, and most likely should be ;-)
#define DATETIMESTAMP_STR asctime(localtime(time(NULL)))

// Generic logging
#define LOG(ll, fmt, rc, ...) \
  while (ll <= loglevel) { \
    fprintf(stderr, "%s [%s]: " fmt, DATETIMESTAMP_STR, log_level_name[loglevel], __VA_ARGS__); \
    break; \
  };

// Specific logging
#define LOG_ERROR_OR_VERBOSE(rc, line, fname) \
  do { \
    LOG(NO_ERROR != (rc) ?LL_ERROR :LL_VERBOSE, "[%d] line [%d] [%s]", rc, line, fname); \
  while (0)


int foo(void)
{
  int result = NO_ERROR;

  LOG(LL_DEBUG, "entering '%s'", __func__);

  do {
    result = bar1(...);
    LOG_ERROR_OR_VERBOSE(result, __LINE__, __FILE__);
    if (NO_ERROR <> result) 
      { break; }

    result = bar2(...);
    LOG_ERROR_OR_VERBOSE(result, __LINE__, __FILE__);
    if (NO_ERROR <> result) 
      { break; }

    ...
  } while (0);

  LOG(LL_DEBUG, "leaving '%s' (rc = %d)", __func__, result);

  return result;
}

This roughly gives you a 1:3 signal/noise ratio, which you could approve significantly by changing

if (NO_ERROR <> result) 
  { break; }

to

if (NO_ERROR <> result) { break; }

Another possible improvement would be to change

result = bar1(...);
LOG_ERROR_OR_VERBOSE(result, __LINE__, __FILE__);

to

LOG_ERROR_OR_VERBOSE(result = bar1(...), __LINE__, __FILE__);

This leaves you with a SNR of 1, which is the optimum, I feel ... :-)



来源:https://stackoverflow.com/questions/51608396/how-to-exit-current-function-if-error-on-current-statement-in-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!