转载请注明出处:http://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_*
的作用就是将对应位置的参数值绑定到预备查询语句字符串当中的“?”处。例如SELECT * FROM t WHERE c1=? AND c2=? AND c3=?
,里面有三个参数需要填入值。这里我们假设列c1是int类型,列c2是double类型,列c3是字符串类型。
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), std::index_sequence_for(), 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
声明了Args是一个类型模板的参数包,这个对应于传统的类型模板template
声明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), std::index_sequence_for(), args...);
}
这还是一个带参数包的函数模板,F对应于将要传入的lambda的类型,Args跟之前一样是一个类型模板参数包。
接下来就是调用第一个工具函数。
第一个参数是将要传入的lambda,代表怎么处理这些个参数。std::forward
完美转发,这是C++11右值的一个重要特性,可以单开一篇写,这里可以先忽略它。
第二个参数std::index_sequence_for
用到了C++14的编译期整数序列特性。在C++14的头文件中有以下定义:
template<class T, T N>
using make_integer_sequence = std::integer_sequence/* 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
,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类型,因此展开时用逗号表达式,先执行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