C++案例分析 – SQLite3 C API的C++封装(下)

转载请注明出处:https://cyc.wiki/index.php/2017/07/14/cpp-case-sqlite3-cpp-2/


永远不要说“精通C++”。


上一篇介绍了怎么用C++的特性优雅地封装SQLite3中的sqlite3_bind_*函数们。这一篇我们继续来看看怎么优雅地改写sqlite3_column_*函数们。

Github: SQLiteC++ (SQLiteCpp)

sqlite_column_*

在执行了SQL查询语句后,通过会使用sqlite3_step函数去逐步遍历查询结果的各行,而获取该行的各列数据的函数就是sqlite3_column_*。我们还是取上一篇的例子,假设数据表中第1列是int类型,第2列是double类型,第3列是字符串类型。

C API

const void *sqlite3_column_blob(sqlite3_stmt*, int iCol);
int sqlite3_column_bytes(sqlite3_stmt*, int iCol);
int sqlite3_column_bytes16(sqlite3_stmt*, int iCol);
double sqlite3_column_double(sqlite3_stmt*, int iCol);
int sqlite3_column_int(sqlite3_stmt*, int iCol);
sqlite3_int64 sqlite3_column_int64(sqlite3_stmt*, int iCol);
const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
const void *sqlite3_column_text16(sqlite3_stmt*, int iCol);
int sqlite3_column_type(sqlite3_stmt*, int iCol);
sqlite3_value *sqlite3_column_value(sqlite3_stmt*, int iCol);

C API依然是这么简单粗暴,还是每一种类型的数据对应一个API的命名,但不同的是这一回所有函数的参数列表都一模一样,仅仅是返回值不同。还记得上一篇提到函数重载的实现本质上是由于参数列表被加进了函数签名,因此如果还想对不同的类型使用同样的函数就不能使用函数重载的方法了。

那怎么办呢?我们可以使用一个统一的中间的对象接收sqlite3_stmt*int iCol两个参数,然后利用类型转换操作符根据不同的类型调用不同的C API获取对应的数据,完成到目标类型的转换。

我不想记住函数的名称 – 类型转换操作符

我们来看SQLiteCpp中是怎么实现自动获取相应类型的结果的。
假设我们已经调用过sqlite3_step将游标移到了当前结果行,现在我们调用stmt.getColumn(index)获得一个Column对象:

Column Statement::getColumn(const int aIndex)
{
    checkRow();
    checkIndex(aIndex);

    // Share the Statement Object handle with the new Column created
    return Column(mStmtPtr, aIndex);
}

这个函数的内容很简单,除了一些合法检查,就是构造了一个Column对象并返回,Column对象是构造参数是stmt的指针和列序号。

Column::Column(Statement::Ptr& aStmtPtr, int aIndex) noexcept : // nothrow
    mStmtPtr(aStmtPtr),
    mIndex(aIndex)
{
}

而Column对象的构造函数更是干净,直接把传进来的参数在初始化列表里一存,留下两个空空的大括号。可能你会纳闷这机关究竟在哪呢?当然现在卖这个关子没什么意义,上面都已经剧透过了,就是在类型转换操作符里。

    /// Inline cast operator to int
    inline operator int() const
    {
        return getInt();
    }
    /// Inline cast operator to 32bits unsigned integer
    inline operator unsigned int() const
    {
        return getUInt();
    }
    /// Inline cast operator to 64bits integer
    inline operator long long() const
    {
        return getInt64();
    }
    /// Inline cast operator to double
    inline operator double() const
    {
        return getDouble();
    }
    /// Inline cast operator to char*
    inline operator const char*() const
    {
        return getText();
    }
    /// Inline cast operator to void*
    inline operator const void*() const
    {
        return getBlob();
    }

这里面的get函数相信大家也能猜到怎么写了:

// Return the integer value of the column specified by its index starting at 0
int Column::getInt() const noexcept // nothrow
{
    return sqlite3_column_int(mStmtPtr, mIndex);
}

// Return the unsigned integer value of the column specified by its index starting at 0
unsigned Column::getUInt() const noexcept // nothrow
{
    return static_cast<unsigned>(getInt64());
}

// Return the 64bits integer value of the column specified by its index starting at 0
long long Column::getInt64() const noexcept // nothrow
{
    return sqlite3_column_int64(mStmtPtr, mIndex);
}

// Return the double value of the column specified by its index starting at 0
double Column::getDouble() const noexcept // nothrow
{
    return sqlite3_column_double(mStmtPtr, mIndex);
}

// Return a pointer to the text value (NULL terminated string) of the column specified by its index starting at 0
const char* Column::getText(const char* apDefaultValue /* = "" */) const noexcept // nothrow
{
    const char* pText = reinterpret_cast<const char*>(sqlite3_column_text(mStmtPtr, mIndex));
    return (pText?pText:apDefaultValue);
}

// Return a pointer to the blob value (*not* NULL terminated) of the column specified by its index starting at 0
const void* Column::getBlob() const noexcept // nothrow
{
    return sqlite3_column_blob(mStmtPtr, mIndex);
}

我们最后来看我们的例子里要怎么获取各列的数据:

int a = stmt.getColumn(0);
double b = stmt.getColumn(1);
const char* c = stmt.getColumn(2);

好,总结一下这里发生的事情。
1. stmt.getColumn返回了一个临时构造Column对象,该对象持有获取该列数据所需的参数——stmt指针和列序号,但此时没有真正去获取。
2. 在a/b/c变量初始化过程中发生了隐式的类型转换,触发了int/double/const char*类型转换操作符。
3. 在类型转换操作符中调用了相应的C API获得该列数据,并作为操作符结果返回。

类型转换操作符也可以被显式类型转换触发,例如我想得到b的double值后转换成int,可以这么写:

int bi = static_cast<double>(stmt.getColumn(1)); // warning: double -> int implicit conversion

经过这么封装,不同类型的数据都可以用同一个函数getColumn进行获取。但值得注意的是执行什么类型的C API取决于类型转换的目标类型,显式的还好说,隐式的类型转换有时会忽略而搞错类型;或者是列序号填错导致想要转换的目标类型不是该列的类型,这些都很可能发生C API的失败。所以设计上必须能很好的给出错误信息,以供开发者调试。

为了解决列序号可能填错的问题,有没有更好的办法呢?在数据库应用中,我们通常会把数据表中的一行看作一个整体,以结构体或类的形式保存数据,那有没有办法把结果行直接转换成对应的结构体或类的实例呢?
有,解决的方式很像上一篇第二个问题的解决方法。

我想要参数序号能被自动地填入 – 变长参数模板 + 编译期整数序列

我们来看看SQLiteCpp中怎么让结果行数据直接存入对应的结构体。
getColumns函数定义如下:

    template<typename T, int N>
    T Statement::getColumns()
    {
        checkRow();
        checkIndex(N - 1);
        return getColumns<T>(std::make_integer_sequence<int, N>{});
    }

    // Helper function called by getColums<typename T, int N>
    template<typename T, const int... Is>
    T Statement::getColumns(const std::integer_sequence<int, Is...>)
    {
        return T(Column(mStmtPtr, Is)...);
    }

第一个函数是入口,进来先做了一下合法检查,然后调用第二个工具函数。
上一篇讲过了std::integer_sequence和它的一些推导,可以知道这里传入的参数

std::make_integer_sequence<int, N>

等同于

std::integer_sequence<int, /* a sequence 0, 1, 2, ..., N-1 */>

然后看这个std::integer_sequence传入第二个工具函数以后,帮助推导出了第二个模板参数包Is,即Is参数包的内容为推导为0, 1, 2, …, N-1。
这样,展开参数包Is的语句

return T(Column(mStmtPtr, Is)...);

等同于

return T(Column(mStmtPtr, 0), Column(mStmtPtr, 1), ..., Column(mStmtPtr, N-1));

接下来看我们具体要怎么使用getColumns获得结构体。

为了能接收一个结果行的数据,我们先定义一个结构体:

struct Row {
    int a;
    double b;
    const char* c;
    Row(int a, double b, const char* c) : a(a), b(b), c(c) {}
}

然后调用getColumns,指定模板参数为接收结果的结构体Row和列数3:

auto row = stmt.getColumns<Row, 3>();

根据上面的分析,工具函数中返回的Row对象等同于

return Row(Column(mStmtPtr, 0), Column(mStmtPtr, 1), Column(mStmtPtr, 2));

这里会去调用Row的构造函数,并把每个Column对象隐式转换成相应的参数类型,在隐式转换过程中调用C API获取数据。

参考资料:
Parameter Pack
Integer Sequence


这个案例分析就告一段落,总结一下这里解决问题的思路:
1. 要灵活处理不同的参数类型,可以考虑函数重载或者函数模板。
2. 要灵活处理不同的返回类型,可以考虑类型转换操作符。
3. 要灵活处理不同长度的参数,可以考虑变长参数模板(C++11特性),必要时使用编译期整数序列(C++14特性)作为辅助。


2017.7.18 更新:

之前在分析getColumns函数的时候举到一个例子,通过定义结构体

struct Row {
    int a;
    double b;
    const char* c;
    Row(int a, double b, const char* c) : a(a), b(b), c(c) {}
}

来接收返回的结果行。

不知道大家有没有跟我一样的疑问:为什么这里需要声明一个构造函数呢?如何不声明会怎么样?
我试过把这行构造函数删去,会得到以下编译错误:
error C2440: '<function-style-cast>': cannot convert from 'initializer list' to 'Statement_getColumns_Test::TestBody::Test'
这个错误意味着,在调用

return T(Column(mStmtPtr, Is)...);

时,Column(mStmtPtr, Is)...,类型为initializer list,要先被转换成Test结构体,再调用T(T&&)移动构造函数。而由于不存在initializer list到Test结构体的转换方式,所以报错了。

那有没有办法不需要声明一个构造函数就实现initializer list的直接构造呢?
其实只要把T()改成T{}就可以了:

return T{Column(mStmtPtr, Is)...};

这样有两种用法:
1. 如果没有定义相应参数列表的构造函数,则是以默认initializer list初始化的方式构造结构体,初始化列表里的元素次序与结构体成员定义的次序保持一致。
2. 如果定义了相应参数列表的构造函数,则是以T{}的形式调用构造函数,与T()的行为是基本一致的。

以这个改进建议我向SQLiteCpp项目的作者提出了一个issue,并获得了作者的采纳。作者已经针对我的建议提交了一个fix

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

You are commenting using your WordPress.com account. Log Out /  更改 )

Google photo

You are commenting using your Google account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

Connecting to %s