使用演绎来进行C ++可变扩展

时间:2017-11-16 10:11:01

标签: c++ c++11 c++14 variadic-templates

我正在开发一个处理非类型C函数(SQLite)的库,我想强力输入它。

我们的想法是拥有一个FieldDef强类型,允许用户将原始类型(如int,double和std :: string)绑定到弱db类型。 我的问题是库的语义很重,我想添加一些自动类型推导。

所以我有一堆"基本类型":

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

我还有一个insert和一个query函数,允许在不使用SQL语句的情况下插入和查询表。查询将是纯选择。无论如何。预期用途是:

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

我以这种方式实现了插入:

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

这很好用,问题来自查询:

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

当调用它时,编译器无法正确推断出类型,所以我猜它需要一个迂腐的构造:

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

它不实用且冗长。

  1. 是否可以简化查询功能? 由于我们对使用有约束,那就是Us包只能是与FieldType::*:rawtype兼容的某种类型,我问是否可以使用一些解包并应用方法的构造。如果是insert,可以通过以下方式进行简化:

    template<typename Ts...>
    bool insert (std::tuple<Ts...> def, std::tuple<Ts::rawtype ...> values) 
    
  2. 而不是使用元组,使用Variadic Packs怎么样?我还没有对它进行过测试,但我担心会使用像

    这样的东西
    template<typename Ts..., typename Us....>
    bool insert (Ts... def, Us ... values) 
    
  3. 会使编译器混淆并使事情变得更糟。 你觉得怎么样?

    1. 如果可以使用查询的实际实现,那么什么是使用法代码更具表现力的解决方法?
    2. 以下是有关代码的一些细节,以解释:

      使用以下伪代码实现查询功能:

      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是隐藏sqlite语句结构的opaque类型,query方法newStatement方法executecolumnsCount和{中使用的其他函数也是如此{1}}。函数getAllValues使用递归来填充tuple。因此,将为数据库的每一行调用仿函数resultFeedbackFunc()。因此,客户端代码可以填充容器(如矢量)。

      更新

      我遵循了@ bolov的解决方案,并添加了@ massimiliano-jones的改进。

      这是对反馈函数内部调用的正确实现:

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

      getValueRsqlite_column_xxx(sqlite3_stmt *, int index)进行内部调用。如果我理解正确,解包是有效的,因为参数列表是解包的有效上下文。如果我希望在参数之外进行调用,我必须进行折叠(或者因为我使用c ++ 11而进行解决)。

2 个答案:

答案 0 :(得分:6)

由于您的帖子中缺少重要的代码部分,因此很难提供具体的帮助。

然而,这是我的2美分。请注意,我的想象力充满了代码中缺失的部分。

首先,您需要摆脱express参数。仅在需要提供类型擦除时才使用std::function。在您的情况下(至少从您显示的代码中)您不需要它。所以我们用一个简单的std::function参数替换它。这解决了演绎问题。

现在,当您传递一个无效的函数对象时,您将在template <class F>实现的深层中获得编译错误。如果你不想要那个并且想要快速失败,那么有一些选择。我选择使用query向您展示SFINAE方法。

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
}

关于你的观点2.那是不可扣除的。即使是这样,您也希望将参数分组以便于阅读。即您想要error: no matching function for call to 'query' ... note: candidate template ignored: substitution failure [with Args = ...]: no matching function for call to 'query_impl' ... 而不是insert({a, b}, {c, d})

我不明白你的观点3。

答案 1 :(得分:2)

  
      
  1. 而不是使用元组,使用Variadic Packs怎么样?
  2.   

您可以尝试类似

的内容
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) );

这比元组版本更具可读性:首先,字段数自动等于值计数,然后,每个值都在其字段旁边,它支持默认&#39;字段值(例如,通过mName()),...

关于query(),更具表现力的替代方案可能是

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