Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ variadic expansion using deduction

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.

  1. 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) 
    
  2. 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?

  1. 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).

like image 648
HappyCactus Avatar asked Nov 16 '17 10:11

HappyCactus


2 Answers

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.

like image 150
bolov Avatar answered Oct 04 '22 07:10

bolov


  1. 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*/);
like image 22
Massimiliano Janes Avatar answered Oct 01 '22 07:10

Massimiliano Janes