2017-11-16 48 views
7

Zajmuję się tworzeniem biblioteki, która zajmuje się nie wpisywanymi funkcjami C (SQLite) i chcę ją mocno napisać.Rozszerzanie variadyczne C++ za pomocą dedukcji

Jest to silny typ, który umożliwia użytkownikowi powiązanie typów surowych takich jak int, double i std :: string ze słabymi typami db. Moim problemem jest to, że semantyczna biblioteka jest bardzo ciężka i chciałbym dodać automatyczne odliczanie typów.

Mam więc kilka "podstawowych typów":

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

Mam też insert a query funkcji, które umożliwiają wstawianie i odpytywanie bez użycia tabel SQL. Zapytanie będzie proste. Tak czy inaczej. Zamierzone zastosowanie ma:

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 wdrożone wkładki w ten sposób:

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

Działa to dobrze, problemy pochodzą z kwerendy:

template <typename ...Ts, typename ...Us> 
void query(std::tuple<Ts...> def, std::function<void(std::tuple<Us...>)> resultFeedbackFunc) { 
    ... 
} 

Kiedy nazywając go kompilator jest w stanie poprawnie wyprowadzić typy, więc myślę, że wymaga pedantycznej konstrukcji:

SQLiteTable::query<FieldType::Text, FieldType::Integer, /* whatever */> (...) 

To niepraktyczne i gadatliwe.

  1. Czy można uprościć funkcję zapytania? Ponieważ mamy ograniczenie w użyciu, to jest to pakiet Us, który może być tylko zgodny z typem FieldType::*:rawtype. Pytam, czy możliwe byłoby użycie jakiegoś konstruktu, który rozpakowuje i stosuje metodę. W przypadku insert, to może być uproszczone coś takiego:

    template<typename Ts...> 
    bool insert (std::tuple<Ts...> def, std::tuple<Ts::rawtype ...> values) 
    
  2. Zamiast używać krotek, co na temat korzystania pakiety o zmiennej liczbie argumentów? Nie testowałem go, ale obawiam się, że przy użyciu coś jak

    template<typename Ts..., typename Us....> 
    bool insert (Ts... def, Us ... values) 
    

byłoby pomylić kompilator i będzie pogorszyć sytuację. Co myślisz?

  1. Jeśli możliwe jest zastosowanie rzeczywistej implementacji zapytania, jakie byłoby obejście, aby kod użycia był bardziej wyrazisty?

Oto kilka szczegółów na temat kodu, aby wyjaśnić:

Funkcja zapytanie jest realizowane za pomocą następującego Pseudokod:

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 jest nieprzezroczysty typ, który ukrywa sqlite struktura instrukcji, podobnie jak inne funkcje stosowane w metodzie query, Funkcja getAllValues wykorzystuje rekursję do wypełnienia tuple. Tak więc funktor resultFeedbackFunc() będzie wywoływany dla każdego wiersza bazy danych. Zatem kod klienta może na przykład wypełnić kontener (jak wektor).


Aktualizacja:

Śledziłem rozwiązanie @ bolov, a dodane ulepszenia @ Massimiliano-Jones.

Jest to poprawne wdrożenie wewnętrznego wywołanie funkcji sprzężenia zwrotnego:

resultFeedbackFunc(getValueR<decltype (std::get<Is>(def).rawType())> 
     (stmt.get(), Is)...); 

getValueR sprawia wewnętrzne wezwanie do sqlite_column_xxx(sqlite3_stmt *, int index). Jeśli rozumiem poprawnie, rozpakowanie działa, ponieważ lista argumentów jest ważnym kontekstem rozpakowywania. Gdybym chciał, aby wywołania były wykonywane poza argumentami, musiałem wykonać składanie (lub obejść, ponieważ używam C++ 11).

+0

dla zainteresowanych, projekt opublikowany jest tutaj: https://github.com/studiofuga/mSqliteCpp – HappyCactus

Odpowiedz

6

Trudno jest udzielić konkretnej pomocy, ponieważ brakuje ważnych części kodu ze swojego wpisu.

Jednak tutaj są moje 2 centy. Proszę zauważyć, że wypełniłem moją wyobraźnię brakującymi częściami twojego kodu.

Przede wszystkim musisz pozbyć się argumentu std::function. Użyj std::function tylko wtedy, gdy potrzebujesz usunięcia typu, który zapewnia. W twoim przypadku (przynajmniej z kodu, który pokazałeś) nie potrzebujesz tego. Zamienimy to na prosty parametr template <class F>. To rozwiązuje problem dedukcji.

Teraz, gdy zdasz nieprawidłowy funktor, dostaniesz błąd kompilacji głęboko w misach swojej implementacji query. Jeśli tego nie chcesz i chcesz szybko się zawieść, jest kilka opcji. Zdecydowałem się pokazać podejście SFINAE z 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 
} 

Nieprawidłowe połączenia niepowodzeniem z komunikatem zbliżona do:

error: no matching function for call to 'query' 
... 
note: candidate template ignored: substitution failure [with Args = ...]: 
    no matching function for call to 'query_impl' 
... 

dotycząceTwojego punkt 2. To byłoby nie podlega odliczeniu. A nawet gdyby tak było, chcesz zgrupować parametry dla czytelności. To znaczy. chcesz insert({a, b}, {c, d}) zamiast insert(a, b, c, d).

Nie rozumiem twojego punktu 3.

+0

Dziękuję bardzo! To jest rodzaj interfejsu, o którym myślałem. W pytaniu podam jeszcze trochę szczegółów, ale większość założeń, które zrobiłeś, jest w porządku. Staram się dostosować twoją strukturę do rzeczywistego kodu, ale myślę, że mam już wszystkie potrzebne informacje, dziękuję. O 2), ok jak się domyślałem. zapomnij o 3), ponieważ mamy eleganckie rozwiązanie, nie potrzebujemy żadnego obejścia. – HappyCactus

2
  1. zamiast używać krotek, co z używaniem pakietów Variadic?

można spróbować coś takiego

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

jest bardziej czytelny niż wersja krotki: po pierwsze, pola liczyć się automatycznie równa wartości liczą więc, każda wartość zbliża się do swojej dziedzinie, wspiera 'default' wartości pól (przez, powiedzmy, mName()), ...


dotyczące query(), bardziej wyraziste alternatywy m rawo być

// 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*/); 
+0

Podoba mi się twoja implementacja dla 'insert', thanks. O 'zapytaniu', próbuję zaimplementować rozwiązanie @ bolov, ale jestem zapchany budowaniem połączenia z funktorem :-) Dzięki. – HappyCactus