使用演绎的C++变量展开
C++ variadic expansion using deduction
我正在开发一个处理非类型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 */> (...)
这既不切实际又冗长。
是否可以简化查询功能?由于我们在用法上有一个限制,即
Us
包只能是与FieldType::*:rawtype
兼容的某种类型,我想问是否可以使用一些解包并应用方法的构造。在insert
的情况下,它可以用类似于的东西来简化吗template<typename Ts...> bool insert (std::tuple<Ts...> def, std::tuple<Ts::rawtype ...> values)
与其使用元组,不如使用变分包?我还没有测试过,但我担心使用之类的东西
template<typename Ts..., typename Us....> bool insert (Ts... def, Us ... values)
会混淆编译器,使情况变得更糟。你觉得怎么样?
- 如果可以使用查询的实际实现,那么有什么变通方法可以使使用代码更具表达性
以下是有关代码的一些详细信息,以解释:
查询功能使用以下伪代码实现:
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
语句结构的不透明类型,query
方法newStatement
、execute
和columnsCount
中使用的其他函数也是如此。函数getAllValues
使用递归来填充tuple
。因此,将为数据库的每一行调用函子resultFeedbackFunc()
。因此,例如,客户端代码可以填充一个容器(像向量一样)。
更新:
我遵循了@bolov的解决方案,并添加了@massimiliano-jones的改进。
这是对反馈函数内部调用的正确实现:
resultFeedbackFunc(getValueR<decltype (std::get<Is>(def).rawType())>
(stmt.get(), Is)...);
CCD_ 16对CCD_ 17进行内部调用。如果我理解正确的话,解包是有效的,因为参数列表是解包的有效上下文。如果我想在参数之外进行调用,我必须进行折叠(或者变通方法,因为我使用的是c++11)。
很难提供特定的帮助,因为您的文章中缺少重要的代码部分。
不过这是我的2美分。请注意,我已经用我的想象力填充了您代码中缺失的部分。
首先,您需要摆脱std::function
的争论。只有当您需要std::function
提供的类型擦除时,才使用它。在您的情况下(至少从您显示的代码来看),您不需要它。因此,我们将其替换为一个简单的template <class F>
参数。这就解决了演绎问题。
现在,当您传递一个无效的函数对象时,您将在query
实现的碗中得到一个编译错误。如果你不想那样,你想快速失败,那么有一些选择。我选择向您展示decltype
的SFINAE方法。
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
}
无效呼叫失败,并显示类似于的消息
error: no matching function for call to 'query' ... note: candidate template ignored: substitution failure [with Args = ...]: no matching function for call to 'query_impl' ...
关于您的观点2。这是不可扣除的。即使是这样,为了可读性,您也需要对参数进行分组。也就是说,你想要insert({a, b}, {c, d})
而不是insert(a, b, c, d)
。
我不理解你的观点。
- 不使用元组,不如使用Variadic Pack
您可以尝试类似的东西
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) );
这比元组版本更可读:首先,字段计数自动等于值计数,然后,每个值都位于其字段旁边,它支持"默认"字段值(例如,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*/);
- 如何创建一个CMake变量,除非显式重写,否则使用默认值
- 将成员变量添加到共享库中的类中,不会破坏二进制兼容性吗
- 将数组的地址分配给变量并删除
- 为"adjacent"变量赋值时出现问题
- enum是C++中的宏变量还是整数变量
- 在全局变量中保存类的实例以重新创建类(创建"backup")
- 用C++中的一个变量定义一个常量
- 具有奇怪重复模板模式的派生类中的成员变量已损坏
- 你能重载对象变量名本身返回的内容吗
- 内置函数可查看CPP中的成员变量
- 是否可以初始化不可复制类型的成员变量(或基类)
- 尝试通过多个向量访问变量时,向量下标超出范围
- 试图让变量检查数组中的某些内容
- Cpp-Tuple使用带有变量的get
- 将包含C样式数组的对象初始化为成员变量(C++)
- 当vector是tje全局变量时,c++中vector的内存管理
- 通过多个头文件使用常量变量
- std::threads可以从Windows DLL中的全局变量创建/销毁吗?
- 执行函数时导致崩溃的变量
- 变量没有改变?通过向量的函数调用