对于Sqlite的C / C ++中的标量函数,更好的方法是什么?

时间:2013-12-18 12:21:45

标签: c++ sqlite

我作为C#程序员具有扎实的背景,但没有使用C / C ++的经验。说,我正在尝试创建一个标量函数来返回一个对象,无论数据类型如何,都通过如下所示的void指针返回。

void * DB::Scalar(char * sqlCmd)
{   
sqlite3_prepare_v2(db, sqlCmd, -1, &stmt, NULL);

void *obj = NULL;

if(sqlite3_step(stmt) == SQLITE_ROW)
{   
    if(sqlite3_column_type(stmt, 0) == SQLITE_INTEGER)
    {           
        int resInt = sqlite3_column_int(stmt, 0);
        obj = (void *)&resInt;

    }else if(sqlite3_column_type(stmt, 0) == SQLITE_TEXT)
    {   
                    //when column type is text copies string, so I can call
                    //sqlite3_finalize before return;

        char* tmp = (char* )sqlite3_column_text(stmt, 0);
        int len = strlen(tmp) + 1;
        char* resChar = (char*)malloc(len);
        memset(resChar, 0, len);
        strcpy(resChar, tmp);

        obj = (void *)resChar;

    }else if(sqlite3_column_type(stmt, 0) == SQLITE_FLOAT)
    {   
        double resDoub = sqlite3_column_double(stmt, 0);
        obj = (void *)&resDoub;
    }

    sqlite3_finalize(stmt);

    return obj;//return void pointer and whoever calls it is responsible for
                       //the appropriate cast
}
}

那看起来不错? 我测试过并且工作得非常好。

3 个答案:

答案 0 :(得分:4)

在这段代码中,您有未定义的行为:

if(sqlite3_column_type(stmt, 0) == SQLITE_INTEGER)
{           
    int resInt = sqlite3_column_int(stmt, 0);
    obj = (void *)&resInt;
}

问题在于resInt变量的范围仅在{}之间的那个块内。一旦代码离开该块,该变量不再在范围内,并且任何指向该变量的指针都不再有效。

答案 1 :(得分:2)

由于C ++不包含任何真正意义上的类型内省功能,因此您可能希望使用模板函数(或其中的一族)。我可能首先将sqlite3_stmt管理包装在资源类中,如:

class sqlite3_statement {
public:
    sqlite3_statement(sqlite3 *db, std::string const& query) {
        sqlite3_prepare_v2(db, query.c_str(), -1, &stmt_handle, NULL);
    }
    ~sqlite3_statement() {
        sqlite3_finalize(stmt_handle);
    }
    bool get_next_row() {
        return sqlite3_step(stmt_handle) == SQLITE_ROW;
    }
    int column_type(int index) {
        return sqlite3_column_type(stmt_handle, index);
    }
    operator sqlite3_stmt*() { return stmt_handle; }
private:
    sqlite3_stmt *stmt_handle;
};

这将通过RAII原则使资源管理更清洁,更安全。然后,您可以将query_for_scalar实现为一系列模板函数。请注意,您需要为要支持的每个标量类型显式专门化。

template <typename T> T query_for_scalar(std::string const& query);

template <> int
query_for_scalar<int>(std::string const& query)
{
    sqlite3_statement statement(db, query);

    if (statement.get_next_row()) {
        if (statement.column_type(0) != SQLITE_INTEGER) {
            throw runtime_error("invalid column type");
        }
        return sqlite_column_int(statement, 0);
    } else {
        throw runtime_error("no row retrieved");
    }
}


template <> float
query_for_scalar<float>(std::string const& query)
{
    sqlite3_statement statement(db, query);

    if (statement.get_next_row()) {
        if (statement.column_type(0) != SQLITE_FLOAT) {
            throw runtime_error("invalid column type");
        }
        return sqlite_column_float(statement, 0);
    } else {
        throw runtime_error("no row retrieved");
    }
}

类型映射也可以使用转换器类型完成。这样可能会有点干净。在任何一种情况下,这都将避免其他海报提到的UB,这是一个更加C ++惯用的解决方案。

答案 2 :(得分:1)

首先,没有任何C / C ++语言。当然,这并不意味着您不能混合C和C ++构造,或者使用C ++中的C API(您可以使用来自几乎任何体面语言的C API);但它确实意味着响应应该是C还是C ++会对程序产生重大影响。

目前,你所拥有的是一个带有非常不安全的界面的错误C函数。由于我们谈论的是C,因此我们普遍接受不安全,但我们可以做得更好。

typedef enum { None, Int, CString } Kind_t;

typedef struct Result { Kind_t kind; void* item; } Result_t;

// Note: the memory pointed by Result_t::item, if any, falls
//       under the responsibility of the caller.
Result_t db_scalar(char const* sqlCmd) {
    // where do db and stmt come from ?
    sqlite3_prepare_v2(db, sqlCmd, -1, &stmt, NULL);

    Result_t result = { None, 0 };

    if (sqlite3_step(stmt) != SQLITE_ROW) { goto exit; }

    if (sqlite3_column_type(stmt, 0) == SQLITE_INTEGER)
    {
        int* i = (int*) malloc(sizeof(int));
        if (i == 0) { goto exit; }

        *i = sqlite3_column_int(stmt, 0);
        result = { Int, i };

        goto exit;
    }

    if (sqlite3_column_type(stmt, 0) == SQLITE_TEXT)
    {
        char const* tmp = (char*) sqlite3_column_text(stmt, 0);
        if (tmp == 0) { goto exit; }

        size_t const len = strlen(tmp) + 1;

        char* text = (char*) malloc(len);
        if (text == 0) { goto exit; }

        strncpy(text, tmp, len);
        result = { CString, text };

        goto exit;
    }

exit:
    sqlite3_finalize(&stmt);
    return result;
} // db_scalar

我对您的初始提案做了一些更改:

  • 结果类型现在指示元素的类型,因此调用者不必猜测
  • 调用者负责item
  • 指向的内存
  • 虽然我们保留单个退出点以进行清理,但是这些语句已被取消。
  • 在可以预期的地方进行各种空检查

C ++是什么?好吧,事情以一种有趣的方式发生变化。

// RAII class to always finalize a statement
class StatementFinalizer {
public:
    explicit StatementFinalizer(sqlite3_stmt& stmt): stmt(stmt) {}
    ~StatementFinalizer() { sqlite3_finalize(&stmt); }
private:
    sqlite3_stmt& stmt;
}; // class StatementFinalizer


using Result = boost::variant<boost::none_t, int, std::string>;

Result DB::scalar(std::string const& sqlCmd) {
    sqlite3_prepare_v2(db, sqlCmd.c_str(), -1, &stmt, nullptr);
    StatementFinalizer const deferred(stmt);

    if (sqlite3_step(stmt) != SQLITE_ROW) { return boost::none; }

    if (sqlite3_column_type(stmt, 0) == SQLITE_INTEGER) {
        return sqlite3_column_int(stmt, 0);
    }

    if (sqlite3_column_type(stmt, 0) == SQLITE_TEXT) {
        unsigned char const* text = sqlite3_column_text(stmt, 0);
        return text ? std::string(text) : std::string("");
    }

    return boost::none;
} // DB::scalar

您可能会注意到C ++解决方案明显缩短了。它也更有效(例如int无分配)。

另一方面,它的结构基本相同,我们只是通过使用析构函数的功能来回避单一退出问题,总是在退出时执行一个动作。