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 Us
pack can only be some type compatible with FieldType::*:rawtype
, I am asking if it would be possible to use some construct that unpacks and apply a method. In case of insert
, 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?
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).
It's difficult to give specific help as there are important code parts missing from your post.
However here are my 2 cents. Please note I've filled with my imagination the missing parts of your code.
First of all you need to get rid of the std::function
argument. Use std::function
only if you need the type erasure it provides. In your case (at least from the code you've shown) you don't need it. So we replace that with a simple template <class F>
parameter. This solves the deduction problem.
Now when you pass an invalid function object you will get a compilation error deep in the bowls of your query
implementation. If you don't want that and you want to fail fast, then there are some options. I chose to show you a SFINAE approach with decltype
.
namespace FieldType
{
struct Integer { using rawtype = int; };
struct Real{ using rawtype = double; };
struct Text{ using rawtype = std::string; };
};
template <class FT>
struct FieldDef
{
using Type = FT;
using RawTye = typename Type::rawtype;
auto getRaw() -> RawTye { return {}; }
};
template <class... Args, class F, std::size_t... Is>
auto query_impl(std::tuple<Args...> def, F f, std::index_sequence<Is...>)
-> decltype(f(std::get<Is>(def).getRaw()...), std::declval<void>())
{
f(std::get<Is>(def).getRaw()...);
}
template <class... Args, class F>
auto query(std::tuple<Args...> def, F f)
-> decltype(query_impl(def, f, std::make_index_sequence<sizeof...(Args)>{}))
{
query_impl(def, f, std::make_index_sequence<sizeof...(Args)>{});
}
auto test()
{
FieldDef<FieldType::Text> mName = {};
FieldDef<FieldType::Integer> mValue = {};
query(std::make_tuple(mName, mValue), [](std::string, int) {}); // OK
query(std::make_tuple(mName, mValue), [](std::string, int, int) {}); // Error
query(std::make_tuple(mName, mValue), [](int, int) {}); // Error
}
The invalid calls fail with a message akin to:
error: no matching function for call to 'query' ... note: candidate template ignored: substitution failure [with Args = ...]: no matching function for call to 'query_impl' ...
Regarding your point 2. That would be not deductible. And even if it would be, you want to group the parameters for readability. I.e. you want insert({a, b}, {c, d})
instead of insert(a, b, c, d)
.
I don't understand your point 3.
- instead of using tuples, what about using Variadic Packs?
you may try something like
template<typename T,typename V>
struct FieldInsert{ V const& ref; };
template<typename T>
struct FieldDef
{
template<typename V>
auto operator()( V const& value ) const{ return FieldInsert<T,V>{value}; }
};
template<typename... T>
bool insert( T... args )
{
// defines buildSqlInsertFieldList and buildSqlInsertValuesListPlaceholder the obvious way ...
}
// to be used as
SQLiteTable::insert( mName(record.name), mValue(record.value) );
this is more readable than the tuple version: firstly, fields count is automatically equal to values count, then, each value comes next to its field, it supports 'default' field values ( via, say, mName()
), ...
regarding query()
, more expressive alternatives might be
// return a lazily evaluated input-range (or a coroutine?)
for( auto item: SQLiteTable::query( mName, mValue ) )
// ...
// and/or query() simply returns a forwarding wrapper exposing, say, a chainable interface ...
SQLiteTable::query( mName, mValue ).for_each([]/*lambda*/);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With