I'm developing a library that deals with non typed C functions (SQLite) and I want to strong typing it.
The idea is to have a FieldDef strong type that allow user to tie raw types like int, double and std::string to weak db types.
My problem is that the semantic of the library is very heavy, and I would like to add some automatic type deduction.
So I have a bunch of "basic types":
namespace FieldType {
struct Integer { using rawtype = int; };
struct Real{ using rawtype = double; };
struct Text{ using rawtype = std::string; };
struct Blob{ using rawtype = std::vector<uint8_t>; };
}
I also have a insert and a query functions that allow inserting and querying the tables without using SQL statements. The query will be plain select. Anyway. The intended use is:
FieldDef<FieldType::Integer> mId = makeFieldDef("id", FieldType::Integer()).primaryKey().autoincrement();
FieldDef<FieldType::Text> mName = makeFieldDef("name", FieldType::Text());
FieldDef<FieldType::Integer> mValue = makeFieldDef("value", FieldType::Integer());
SQLiteTable::insert(std::make_tuple(mName, mValue), std::make_tuple(record.name, record.value));
std::vector<Record> r;
SQLiteTable::query
(std::make_tuple(mName, mValue), [&r](std::tuple<std::string, int> res) {
r.push_back(Record{std::get<0>(res), std::get<1>(res)});
});
I implemented the insert this way:
template <typename ...Ts, typename ...Us>
bool insert (std::tuple<Ts...> def, std::tuple<Us...> values) {
std::ostringstream ss;
ss << "INSERT INTO " << mName << "("
<< buildSqlInsertFieldList<0>(def)
<< ") VALUES ("
<< buildSqlInsertValuesListPlaceholder<0>(values)
<< ");";
auto stmt = newStatement(ss.str());
bindAllValues<0>(stmt.get(), values);
return execute(stmt.get());
}
This works fine, the problems come with query:
template <typename ...Ts, typename ...Us>
void query(std::tuple<Ts...> def, std::function<void(std::tuple<Us...>)> resultFeedbackFunc) {
...
}
When calling it the compiler is unable to correctly deduce the types, so I guess it requires a pedantic construction:
SQLiteTable::query<FieldType::Text, FieldType::Integer, /* whatever */> (...)
It's unpractical and verbose.
Would it be possible to simplify the query function? Since we have a constrain in the usage, that is the
Uspack can only be some type compatible withFieldType::*:rawtype, I am asking if it would be possible to use some construct that unpacks and apply a method. In case ofinsert, could it be simplified with something like:template<typename Ts...> bool insert (std::tuple<Ts...> def, std::tuple<Ts::rawtype ...> values)instead of using tuples, what about using Variadic Packs? I haven't tested it but I fear that using something like
template<typename Ts..., typename Us....> bool insert (Ts... def, Us ... values)
would confuse the compiler and would make things worse. What do you think?
- If it is possible to use the actual implementation of query, what would be a workaround to make the usage code more expressive?
Here are some details about the code, to explain:
The query function is implemented using the following pseudocode:
template <typename ...Ts, typename ...Us>
void query(std::tuple<Ts...> def, std::function<void(std::tuple<Us...>)> resultFeedbackFunc) {
std::ostringstream ss;
ss << "SELECT " << buildSqlInsertFieldList<0>(def) << " FROM " << mName <<";";
auto stmt = newStatement(ss.str());
auto r = execute(stmt.get());
SQLiteException::throwIfNotOk(r, db()->handle());
while (hasData(stmt.get())) {
auto nColumns = columnCount(stmt.get());
if (nColumns != sizeof...(Ts))
throw std::runtime_error("Column count differs from data size");
std::tuple<Us...> res;
getAllValues<0>(stmt.get(), res);
resultFeedbackFunc(res);
}
};
Statement is an opaque type that hides the sqlite statement structure, as do the other functions used in the query method newStatement, execute and columnsCount. The function getAllValues uses recursion to fill the tuple. So the functor resultFeedbackFunc() will be called for every row of the database. So the client code can, for example, fill a container (like a vector).
Update:
I followed @bolov's solution, and added @massimiliano-jones's improvements.
This is the correct implementation of the inner call to the feedback function:
resultFeedbackFunc(getValueR<decltype (std::get<Is>(def).rawType())>
(stmt.get(), Is)...);
getValueR makes the internal call to sqlite_column_xxx(sqlite3_stmt *, int index). If I understand correctly, the unpacking works because the arguments list is a valid context for unpacking. If I wanted the calls to be made outside the arguments, I had to make a folding (or workaround since I'm using c++11).