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

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


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


这其实是来源于我的一个工作项目,其中一个需求就是用C++将SQLite3原来的C API进行一下封装。当时封装做的也就那样,参考了一些别的实现,勉强能用,但是觉得C++封装后做出来的东西确实很优雅。刚好在Github上找到了类似的项目和实现方法,就拿出来解析一下。

Github: SQLiteC++ (SQLiteCpp)

主要拿SQLite3最常用的其中两个操作做例子:sqlite3_bind_*sqlite3_column_*

sqlite3_bind_*

sqlite3_bind_*的作用就是将对应位置的参数值绑定到预备查询语句字符串当中的“?”处。例如SELECT * FROM t WHERE c1=? AND c2=? AND c3=?,里面有三个参数需要填入值。这里我们假设列c1是int类型,列c2是double类型,列c3是字符串类型。

C API

int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*));
int sqlite3_bind_blob64(sqlite3_stmt*, int, const void*, sqlite3_uint64,
                        void(*)(void*));
int sqlite3_bind_double(sqlite3_stmt*, int, double);
int sqlite3_bind_int(sqlite3_stmt*, int, int);
int sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
int sqlite3_bind_null(sqlite3_stmt*, int);
int sqlite3_bind_text(sqlite3_stmt*,int,const char*,int,void(*)(void*));
int sqlite3_bind_text16(sqlite3_stmt*, int, const void*, int, void(*)(void*));
int sqlite3_bind_text64(sqlite3_stmt*, int, const char*, sqlite3_uint64,
                         void(*)(void*), unsigned char encoding);
int sqlite3_bind_value(sqlite3_stmt*, int, const sqlite3_value*);
int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n);
int sqlite3_bind_zeroblob64(sqlite3_stmt*, int, sqlite3_uint64);

C API可以说是简单粗暴,每一种类型的参数有一个对应名字的API函数,前两个参数一样,第一个是语句指针,第二个是参数的序号,后面的参数就跟参数值有关。

于是填入参数需要下面的语句:

sqlite3_bind_int(stmt, 1, 10);
sqlite3_bind_double(stmt, 2, 20.2);
sqlite3_bind_text(stmt, 3, "30", -1, SQLITE_TRANSIENT);

看到这组API,也许你不想要记住函数的名称,并且想要参数序号能被自动地填入。好,那我们来看C++怎么帮你实现。

我不想记住函数的名称 – 函数重载

这应该是C++很基本的一个语言特性了,所以就不多介绍。这里只说下函数重载之所以能在C++中实现,根本原因是C++改变了函数签名的规则,将函数参数列表(不包括返回值)加入了函数签名中,所以不同函数参数列表的同名函数本质上就是不同的符号。C中没有这么做,符号基本上就是函数名本身。在C++中用extern “C”定义的函数,其实就是把函数参数列表从函数签名中扒掉,于是C的程序就可以找到extern “C”定义的函数。

SQLiteCpp做了如下封装:

// Bind an int value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const int aValue)
{
    const int ret = sqlite3_bind_int(mStmtPtr, aIndex, aValue);
    check(ret);
}

// Bind a 32bits unsigned int value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const unsigned aValue)
{
    const int ret = sqlite3_bind_int64(mStmtPtr, aIndex, aValue);
    check(ret);
}

// Bind a 64bits int value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const long long aValue)
{
    const int ret = sqlite3_bind_int64(mStmtPtr, aIndex, aValue);
    check(ret);
}

// Bind a double (64bits float) value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const double aValue)
{
    const int ret = sqlite3_bind_double(mStmtPtr, aIndex, aValue);
    check(ret);
}

// Bind a string value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const std::string& aValue)
{
    const int ret = sqlite3_bind_text(mStmtPtr, aIndex, aValue.c_str(),
                                      static_cast<int>(aValue.size()), SQLITE_TRANSIENT);
    check(ret);
}

// Bind a text value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const char* apValue)
{
    const int ret = sqlite3_bind_text(mStmtPtr, aIndex, apValue, -1, SQLITE_TRANSIENT);
    check(ret);
}

// Bind a binary blob value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const void* apValue, const int aSize)
{
    const int ret = sqlite3_bind_blob(mStmtPtr, aIndex, apValue, aSize, SQLITE_TRANSIENT);
    check(ret);
}

非常简单,就这样使用同样的函数名也能绑定不同类型的函数了。之前的实现代码变成了:

stmt.bind(1, 10);
stmt.bind(2, 20.2);
stmt.bind(3, "30");

我想要参数序号能被自动地填入 – 变长参数模板

变长参数模板(Variadic Template)是C++11引入的一个新特性,可以把模板玩得很花,也很实用。我们先来看下SQLiteCpp里的实现:

/// implementation detail for variadic bind.
namespace detail {
template<class F, class ...Args, std::size_t ... I>
inline void invoke_with_index(F&& f, std::integer_sequence<std::size_t, I...>, const Args& ...args)
{
    std::initializer_list<int> { (f(I+1, args), 0)... };
}

/// implementation detail for variadic bind.
template<class F, class ...Args>
inline void invoke_with_index(F&& f, const Args& ... args)
{
    invoke_with_index(std::forward<F>(f), std::index_sequence_for<Args...>(), args...);
}
}

template<class ...Args>
void bind(SQLite::Statement& s, const Args& ... args)
{
    static_assert(sizeof...(args) > 0, "please invoke bind with one or more args");

    auto f=[&s](std::size_t index, const auto& value)
    {
        s.bind(index, value);
    };
    detail::invoke_with_index(f, args...);
}

这个实现中还利用了C++14的编译期整数序列(std::integer_sequence)特性,免去了变长参数模板展开常用的递归模式。其实不使用C++14的特性,单用C++11的变长参数模板也能实现自动填写参数序号。

上面两个是工具函数,为最下面的bind函数服务。我们先来看bind函数:

template<class ...Args>
void bind(SQLite::Statement& s, const Args& ... args)
{
    static_assert(sizeof...(args) > 0, "please invoke bind with one or more args");

    auto f=[&s](std::size_t index, const auto& value)
    {
        s.bind(index, value);
    };
    detail::invoke_with_index(f, args...);
}

template<class... Args>声明了Args是一个类型模板的参数包,这个对应于传统的类型模板template<class T>声明T是一个类型模板的参数,在推导模板的时候类型模板参数包可以包含不定长度的模板类型。

const Args& ... args声明了参数args是一个函数参数包,类型为const Args&...,在调用的时候函数参数包可以对应不定长度的函数参数。

sizeof...运算符可以获得参数包args的参数个数。此处args后不带…,说明此处args以参数包形式出现。

f是一个无返回值的lambda表达式,代表着传入index和value后,调用s.bind(index, value)的一段逻辑。

detail::invoke_with_index(f, args...)调用工具函数,参数分别是f和args…,此处args以展开后的参数包形式出现。

接着我们看上面的第二个函数:

template<class F, class ...Args>
inline void invoke_with_index(F&& f, const Args& ... args)
{
    invoke_with_index(std::forward<F>(f), std::index_sequence_for<Args...>(), args...);
}

这还是一个带参数包的函数模板,F对应于将要传入的lambda的类型,Args跟之前一样是一个类型模板参数包。
接下来就是调用第一个工具函数。

第一个参数是将要传入的lambda,代表怎么处理这些个参数。std::forward完美转发,这是C++11右值的一个重要特性,可以单开一篇写,这里可以先忽略它。

第二个参数std::index_sequence_for<Args...>()用到了C++14的编译期整数序列特性。在C++14的头文件中有以下定义:

template<class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;
template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;
template<class... T>
using index_sequence_for = std::make_index_sequence<sizeof...(T)>;

经过上面的推导,最终传进去的就是一个std::integer_sequence<std::size_t, 0, 1, 2, ..., N-1>,N为变长参数的个数。

第三个参数就是展开后的参数包args。

最后我们看第一个工具函数:

template<class F, class ...Args, std::size_t ... I>
inline void invoke_with_index(F&& f, std::integer_sequence<std::size_t, I...>, const Args& ...args)
{
    std::initializer_list<int> { (f(I+1, args), 0)... };
}

模板参数中,第一个为类型模板参数F,第二个为类型模板参数包Args,第三个为非类型模板参数包I,I对应于一组类型为std::size_t的数值。

函数参数中,第一个对应将要传入的lambda,第二个是编译期整数序列,其实就是上面传进来的0, 1, 2, …, N-1,用于推导中模板参数中的非类型模板参数包I,第三个是参数包args。

结合上面的分析,f是处理参数的逻辑,args是参数列表,I中含有各参数对应的序号,看起来万事大吉,只要展开调用就完事了。就像这样:

(f(I+1, args))...;

结果会收到一个编译错误,说I和args在这里必须要展开。可是我已经把整个表达式用…展开了啊?事情没有那么简单,参数包展开的位置是有严格的限制的,基本只在函数调用的参数列表里可以展开。但也还有一种情况,可以在初始化列表里的大括号里展开,并且这里还有一个好处,初始化列表里的大括号里展开能保证顺序执行。于是就写成这样:

std::initializer_list<int> { (f(I+1, args), 0)... };

这个意思是用初始化列表初始化一个匿名的std::initializer_list<int>(其实大括号初始化列表本身就是这个类型)。由于列表的元素必须是int类型,因此展开时用逗号表达式,先执行f(I+1, args),再把表达式的值替换成0。展开后的样子就是:

std::initializer_list<int> { (f(1, arg0), 0), (f(2, arg1), 0), ..., (f(N, argN-1), 0) };

顺序执行完所有逗号表达式后其实就是:

std::initializer_list<int> { 0, 0, ..., 0 };

这样既调用了f(1, arg0)到f(N, argN-1),又能符合初始化列表的语法和参数包展开的语法。
详细的讨论参考这个Stackoverflow帖子

最后之前例子的调用变成:

bind(stmt, 10, 20.2, "30");

是不是觉得世界怎么这么美好。

参考资料:
Parameter Pack
Integer Sequence

发表评论

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