C++ Case Analysis - C++ Packaging of SQLite3 C API (Part I)

Keywords: SQL C++ Lambda github

For reprinting, please indicate the source: http://cyc.wiki/index.php/2017/07/12/cpp-case-sqlite3-cpp-1/

Never say "proficient in C++".

This actually comes from one of my work projects. One of the requirements is to encapsulate the original C API of SQLite3 in C++. At that time, the encapsulation did the same, referring to some other implementations, barely able to use, but feel that the C++ encapsulated things really elegant. It happened that similar projects and implementation methods were found on Github, so I took them out and parsed them.

Github: SQLiteC++ (SQLiteCpp)

Take two of the most commonly used operations in SQLite3 as examples: sqlite3_bind_* and sqlite3_column_*.

sqlite3_bind_*

The function of sqlite3_bind_* is to bind the parameter value of the corresponding position to the "?" in the string of the prepared query statement. Place. For example, SELECT * FROM t WHERE c1=? AND c2=? AND c3=?, there are three parameters that need to be filled in. Here we assume that column C1 is of type int, column C2 is of type double, and column C3 is of type string.

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 can be said to be simple and rough. Each type of parameter has an API function with a corresponding name. The first two parameters are the same. The first one is the statement pointer. The second one is the serial number of the parameter. The latter parameters are related to the parameter value.

So filling in the parameters requires the following statement:

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

Looking at this set of API s, maybe you don't want to remember the name of the function, and you want the parameter number to be filled in automatically. Okay, let's see how C++ can help you.

I don't want to remember the name of the function - function overload

This should be a very basic language feature of C++, so I won't introduce it much. The reason why function overloading can be implemented in C++ is that C++ changes the rules of function signature and adds function parameter list (excluding return value) to function signature. So the same-name functions of different function parameter list are essentially different symbols. This is not done in C. Symbols are basically function names themselves. The function defined by extern "C" in C++ is actually to remove the list of function parameters from the function signature, so the program of C can find the function defined by extern "C".

SQLiteCpp is encapsulated as follows:

// 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);
}

Very simply, you can bind different types of functions with the same function name. The previous implementation code becomes:

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

I want the parameter number to be automatically filled in - variable length parameter template

Variadic Template is a new feature introduced by C++11, which can make the template very beautiful and practical. Let's first look at the implementation in 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...);
}

In this implementation, the compile-time integer sequence (std::integer_sequence) feature of C++14 is also used to avoid the recursive mode commonly used in variable-length parameter template expansion. In fact, it does not use the feature of C++14, and can automatically fill in the parameter serial number by using the variable length parameter template of C++11 alone.

The two above are tool functions that serve the bottom bind function. Let's first look at the bind function:

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 > declares that Args is a parameter package of a type template, which corresponds to the traditional type template < class T > declares that T is a parameter of a type template. When deriving a template, the type template parameter package can contain template types of varying lengths.

Const Args &... args declares that parameter args is a function parameter package of type const Args &... and that function parameter packages can correspond to variable length function parameters when called.

The sizeof... operator can obtain the number of parameters in the parameter package args. There is no args with uuuuuuuuuuuu Explain that args appears here in the form of parameter packages.

f is a lambda expression with no return value, representing a logic that calls s s s.bind(index, value) after passing in index and value.

detail::invoke_with_index(f, args...) calls the tool function with parameters F and args... Here args appears in the form of expanded parameter packages.

Then let's look at the second function above:

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...);
}

This is also a function template with parameter packages, F corresponds to the type of lambda to be passed in, and Args is a type template parameter package as before.
The next step is to call the first tool function.

The first parameter is the lambda to be passed in, representing how these parameters are handled. std::forward perfect forwarding, which is an important feature of C++11 right value, can be written in a single article, here you can ignore it first.

The second parameter std:: index_sequence_for <Args...>() uses the compile-time integer sequence feature of C++14. In the header file of C++14, there are the following definitions:

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)>;

After the above deduction, the final input is a std:: integer_sequence < std:: size_t, 0, 1, 2,..., N-1 >, N is the number of variable length parameters.

The third parameter is the expanded parameter package args.

Finally, let's look at the first tool function:

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)... };
}

Among the template parameters, the first is type template parameter F, the second is type template parameter package Args, and the third is type template parameter package I, which corresponds to a set of values of type std::size_t.

Function parameters, the first corresponding to the lambda to be passed in, the second is the compile-time integer sequence, in fact, the above pass in 0, 1, 2,... N-1 is used to derive the untyped template parameter package I in the template parameters, and the third is the parameter package args.

Combined with the above analysis, f is the logic of processing parameters, args is the list of parameters, I contains the corresponding serial number of each parameter, it seems that everything is all right, as long as the call is expanded. Like this:

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

The result is a compilation error that says I and args must be expanded here. But I've used the whole expression... Has it unfolded? It's not as simple as that. There are strict restrictions on the position of parameter package expansion, which can only be expanded in the parameter list of function calls. But there is also a case where expansion can be done in braces in the initialization list, and there is a benefit here that expansion in braces in the initialization list guarantees sequential execution. So it was written as follows:

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

This means to initialize an anonymous std:: initializer_list<int> with the initialization list (in fact, the braces initialization list itself is this type). Since the elements of the list must be of type int, a comma expression is used to expand the list. Firstly, f(I+1, args) is executed, and then the value of the expression is replaced by 0. When it is unfolded, it looks like:

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

When all comma expressions are executed sequentially, they are:

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

This not only calls f(1, arg0) to f(N, argN-1), but also conforms to the syntax of initialization list and parameter package expansion.
Detailed discussion refers to this Stackoverflow posts.

Finally, the invocation of the previous example becomes:

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

Do you think the world is so beautiful?

Reference material:
Parameter Pack
Integer Sequence

Posted by dandaman2007 on Wed, 19 Dec 2018 18:39:05 -0800