如何在没有大量重复检查代码的情况下处理函数中多个可能的早期 returns

How to handle multiple possible early returns from function without lots of duplicate checking code

鉴于我不想在此代码中使用异常,我该如何删除重复的:

if (rc != SQLITE_OK) {
    return rc;
}

在下面这个函数中的大多数语句之后检查?

int sqlite::update(const std::string& table_name, const std::vector<column_values>& fields, const std::vector<column_values>& where) {
    if (db_ == nullptr) {
        return SQLITE_ERROR;
    }
    
    const std::string sql = update_helper(table_name, fields, where);
    sqlite3_stmt* stmt = NULL;
    int rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, NULL);
    if(rc != SQLITE_OK) {
        return rc;
    }

    // loop thru each parameter, calling bind for each one
    rc = bind_fields(stmt, fields);
    if(rc != SQLITE_OK) {
        return rc;
    }

    // loop thru each where parameter, calling bind for each one
    rc = bind_where(stmt, where);
    if (rc != SQLITE_OK) {
        return rc;
    }

    return step_and_finalise(stmt);
}

您可以使用 lambda 和 if-initializer 表达式来清理代码,但您仍然需要 return 错误代码:

int sqlite::update(const std::string& table_name, const std::vector<column_values>& fields, const std::vector<column_values>& where) {
    if (db_ == nullptr) {
        return SQLITE_ERROR;
    }

    const std::string sql = update_helper(table_name, fields, where);
    sqlite3_stmt* stmt = NULL;

    
    const auto validate_sql = [](int rc) -> bool {
        return rc != SQLITE_OK;
    };
    if(int rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, NULL); validate_sql(rc)) {
        return rc;
    }

    // loop thru each parameter, calling bind for each one
    if(int rc = bind_fields(stmt, fields); validate_sql(rc)) {
        return rc;
    }

    // loop thru each where parameter, calling bind for each one
    if(int rc = bind_where(stmt, where); validate_sql(rc)) {
        return rc;
    }

    return step_and_finalise(stmt);
}

借助外部工作,可以编写 monadic 版本。

伪代码如下所示:

expected<void, RC>  doStuff( arg1, arg2 ){
  auto a = op1(arg1, arg2 );
  return a->*op2(arg2)->*op3(arg1);
}

expected<void,RC> doStuff(arg1,arg2){
  auto a = co_await op1(arg1);
  return op2( co_await op3(arg2), co_await op4(arg1, arg2) );
}

但是函数外的样板远远超过函数内删除的样板。

最后,有人提议添加零成本例外。

如果您希望控制流不需要您键入控制流,那么这些和宏是关于您的选项的。

对于预期的情况,想法是预期的错误,当您对其执行 ->* 时,不会调用该操作,而只是通过错误状态。

这是一种没有异常的手动异常形式,但是没有办法自动化样板文件,嗯,在使用点或包装特定于操作集的代码中有更多样板文件。

协程有不可避免的分配开销(好吧,你的代码无法避免它;理论上编译器可以),以及一些最神秘的底层代码。同样,在这里我们试图伪造类似语法的异常而没有异常; co_await 出现错误时,将其传递给 return 值。

TL;DR 是“不,你无法摆脱那个控制流样板”。

首先,如果出现问题,您就是在泄漏 sqlite3_stmt。您需要在 return 之前调用 sqlite3_finalize()

至于您的问题,您可以将重复的 if..return 包装在预处理器宏中,例如:

#define CHECK_RC(rc) \
if (rc != SQLITE_OK) \
{ \
    sqlite3_finalize(stmt); \
    return rc; \
}

int sqlite::update(const std::string& table_name, const std::vector<column_values>& fields, const std::vector<column_values>& where) {
    if (db_ == nullptr) {
        return SQLITE_ERROR;
    }
    
    const std::string sql = update_helper(table_name, fields, where);
    sqlite3_stmt* stmt = NULL;

    int rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, NULL);
    CHECK_RC(rc);

    // loop thru each parameter, calling bind for each one
    rc = bind_fields(stmt, fields);
    CHECK_RC(rc);

    // loop thru each where parameter, calling bind for each one
    rc = bind_where(stmt, where);
    CHECK_RC(rc);

    return step_and_finalise(stmt);
}

或者:

#define CHECK_RC(op) \
{ \
    int rc = op; \
    if (rc != SQLITE_OK) \
    { \
        sqlite3_finalize(stmt); \
        return rc; \
    } \
}

int sqlite::update(const std::string& table_name, const std::vector<column_values>& fields, const std::vector<column_values>& where) {
    if (db_ == nullptr) {
        return SQLITE_ERROR;
    }
    
    const std::string sql = update_helper(table_name, fields, where);
    sqlite3_stmt* stmt = NULL;

    CHECK_RC(sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, NULL));

    // loop thru each parameter, calling bind for each one
    CHECK_RC(bind_fields(stmt, fields));

    // loop thru each where parameter, calling bind for each one
    CHECK_RC(bind_where(stmt, where));

    return step_and_finalise(stmt);
}

或者,您可以重新编写代码以不使用多个 return,例如:

int sqlite::update(const std::string& table_name, const std::vector<column_values>& fields, const std::vector<column_values>& where) {
    int rc;
    if (db_ == nullptr) {
        rc = SQLITE_ERROR;
    }
    else {    
        const std::string sql = update_helper(table_name, fields, where);
        sqlite3_stmt* stmt = NULL;

        rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, NULL);
        if (rc == SQLITE_OK) {
            // loop thru each parameter, calling bind for each one
            rc = bind_fields(stmt, fields);
            if (rc == SQLITE_OK) {
                // loop thru each where parameter, calling bind for each one
                rc = bind_where(stmt, where);
                if (rc == SQLITE_OK) {
                    rc = step_and_finalise(stmt);
                    stmt = NULL;
                }
            }
            sqlite3_finalize(stmt);
        }
    }
    return rc;
}

或者,你可以抛出异常,例如:

void check_rc(int rc)
{
    if (rc != SQLITE_OK)
        throw rc;
}

void check_ptr(void* ptr)
{
    if (ptr == nullptr)
        check_rc(SQLITE_ERROR);
}

int sqlite::update(const std::string& table_name, const std::vector<column_values>& fields, const std::vector<column_values>& where) {
    try {
        check_ptr(db_);
    
        const std::string sql = update_helper(table_name, fields, where);
        sqlite3_stmt* stmt = NULL;

        check_rc(sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, NULL));

        try {
            // loop thru each parameter, calling bind for each one
            check_rc(bind_fields(stmt, fields));

            // loop thru each where parameter, calling bind for each one
            check_rc(bind_where(stmt, where));

            return step_and_finalise(stmt);
        }
        catch (const int) {
            sqlite3_finalize(stmt);
            throw;
        }
    }
    catch (const int errCode) {
        return errCode;
    }
}

在这样的线性操作序列中,我的处理方式与大多数人略有不同:

我不检查上一步是否成功,而是检查我是否具备下一步的先决条件。

int sqlite::update(
    const std::string& table_name,
    const std::vector<column_values>& fields,
    const std::vector<column_values>& where
) {
    sqlite3_stmt* stmt = NULL;

    int rc = SQLITE_ERROR;
    if (db_ != nullptr) {
        const std::string sql = update_helper(table_name, fields, where);
        rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, NULL);
    }
    
    if (rc == SQLITE_OK) {
        // loop thru each parameter, calling bind for each one
        rc = bind_fields(stmt, fields);
    }

    if (rc == SQLITE_OK) {
        // loop thru each where parameter, calling bind for each one
        rc = bind_where(stmt, where);
    }

    if (rc == SQLITE_OK) {
        rc = step_and_finalise(stmt);
    }

    return rc;
}

不可否认,这看起来与原始代码没有太大区别。当每个步骤的先决条件是前面的所有步骤都必须成功时,通常会出现这种情况。在其他示例中,检查看起来并不那么多余。

优点是这与早期 return 解决方案一样线性,没有早期 return 有时引起的混乱和复杂化。在成功的情况下,它不会进行不必要的比较。然而,它没有通常作为早期 return 的替代品出现的深度嵌套。

代价是一个错误将涉及一些冗余检查。但除非错误非常普遍,否则这是一个很小的成本。优化器可能会通过为您创建早期的 return 来消除它们。

这个版本的优点是没有方法外的影响

int sqlite::update(const std::string& table_name, const std::vector<column_values>& fields, const std::vector<column_values>& where) {

    auto check = [](int rc) { if (rc != SQLITE_OK) throw rc; };

    try
    {
        check(db_ == nullptr ? SQLITE_ERROR : SQLITE_OK);
        const std::string sql = update_helper(table_name, fields, where);
        sqlite3_stmt* stmt = NULL;

        check(sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, NULL));

        // loop thru each parameter, calling bind for each one
        check(bind_fields(stmt, fields));

        // loop thru each where parameter, calling bind for each one
        check(bind_where(stmt, where));

        return step_and_finalise(stmt);
    }
    catch (const int rc)
    {
        return rc;
    }
}