将 C++ 字符串解析为元组
Parsing a C++ string into a tuple
我正在开发一个简单的 CSV 解析器,它将文件的行存储在一个元组中。如果不是因为文件中各行的条目数及其类型都是变量,这将是一项简单的任务。因此,这些行可能是这样的:
1,2.2,你好,18,世界
解析器应该能够像这样工作:
ifstream file("input.csv");
SimpleCSVParser<int, float, string, int, string> parser(file);
当我尝试实现一个函数来解析实际行时,事情变得复杂了。我仍然没有找到一种方法来从参数列表中提取下一个类型以在调用 file >> var
之前声明变量。我还需要在循环中执行此操作,以某种方式从每次迭代的结果构建一个元组。
那么如何使用纯 C++11 将字符串解析为元组?
我试过这个:
template <typename ...Targs>
tuple<Targs...> SimpleCSVParser<Targs...>::iterator::operator*() {
istringstream in(cur);
in.imbue(locale(locale(), new commasep)); // for comma separation
tuple<Targs...> t;
for (size_t i = 0; i < sizeof...(Targs); ++i) {
tuple_element<i,decltype(t)>::type first;
in >> first;
auto newt = make_tuple(first);
// what do I do here?
}
}
但是它不起作用,因为我用来提取类型的元组是空的。
你不能像这样使用元组。
This would be an easy task if it wasn't for the fact that the number
of entries on the lines inside the file is a variable, as well as
their type.
据我了解,在处理文件时,您只在 运行 时知道您想要的元组大小和类型。不幸的是,这必须在编译时知道...
如果你真的想使用元组,你必须对你的文件进行预处理以确定数据大小和类型。然后您可以相应地使用正确的元组。但是你不能直接这样做。
for (size_t i = 0; i < sizeof...(Targs); ++i) {
tuple_element<i,decltype(t)>::type first;
in >> first;
auto newt = make_tuple(first);
// what do I do here?
}
这是运行时。您应该考虑使用可变参数模板的递归函数。那将是编译时间。
通过 type erasure 执行此类操作的常用方法,例如使用所有可能值类型的 union
加上指示哪个是实际条目的标志
namespace generic_type {
struct generic
{
enum type { Void=0, Bool=1, Int=2, String=3, Float=4 };
type Type=Void;
union {
bool B;
std::int64_t I;
std::string S;
double X;
}
generic() = default;
generic(generic&&) = default;
generic(generic const&) = default;
generic(bool b) : Type(Bool), B(b) {}
generic(std::int64_t i) : Type(Int), I(i) {}
generic(std::uint64_t i) : Type(Int), I(i) {}
generic(std::string const&s) : Type(String), S(s) {}
generic(std::string &&s) : Type(String), S(std::move(s)) {}
generic(double x) : Type(Float), X(x) {}
};
namespace details {// auxiliary stuff
template<typename T, typename E=void>
struct traits
{
static constexpr generic::type Type=generic::Void;
static void get(generic const&) {}
};
template<>
struct traits<bool,void>
{
static constexpr generic::type Type=generic::Bool;
static bool get(generic const&x) { return x.B; }
};
template<typename T>
struct traits<T,enable_if_t<std::is_integral<T>::value>
{
static constexpr generic::type Type=generic::Int;
static T get(generic const&x) { return x.I; }
};
template<>
struct traits<std::string,void>
{
static constexpr generic::type Type=generic::Str;
static std::string const& get(generic const&x) { return x.S; }
static std::string&& get(generic&&x) { return std::move(x.S); }
};
template<T>
struct traits<float,enable_if<std::is_same<T,float>::value ||
std::is_same<T,double>::value>
{
static constexpr generic::type Type=generic::Float; };
static T get(generic const&x) { return x.X; }
}
}
template<typename T>
auto unsafe_extract(generic const&x)
-> decltype(details::traits<T>::get(x))
{ return details::traits<T>::get(x); }
template<typename T>
auto unsafe_extract(generic&&x)
-> decltype(details::traits<T>::get(std::move(x)))
{ return details::traits<T>::get(std::move(x)); }
template<typename T>
auto extract(generic const&x)
-> decltype(unsafe_extract(x))
{
if(details::traits<T>::Type != x.Type)
throw std::runtime_error("type mismatch in extract(generic)");
return unsafe_extract(x);
}
template<typename T>
auto extract(generic&&x)
-> decltype(unsafe_extract(std::move(x)))
{
if(details::traits<T>::Type != x.Type)
throw std::runtime_error("type mismatch in extract(generic&&)");
return unsafe_extract(std::move(x));
}
}
using generic_type::generic;
然后您可以将数据存储在 std::vector<generic>
。
如果您使用 std::tuple_cat,您应该能够将每个后续值添加到元组中。如果我是你,我还建议使用 C++14 return 类型推导,这样就无需知道 return 类型。
看来,您尝试迭代元组 indices/types,我认为这不起作用。但是,您可以 做的是为每个成员调用一个读取函数。这个想法是将元组的处理委托给一个函数,该函数使用参数包将操作扩展为对每个元素的操作。 std::index_sequence<...>
可用于获取整数序列。
像这样:
template <typename T>
bool read_tuple_element(std::istream& in, T& value) {
in >> value;
return true;
}
template <typename Tuple, std::size_t... I>
void read_tuple_elements(std::istream& in, Tuple& value, std::index_sequence<I...>) {
std::initializer_list<bool>{ read_tuple_element(in, std::get<I>(value))... });
}
template <typename ...Targs>
tuple<Targs...> SimpleCSVParser<Targs...>::iterator::operator*() {
std::istringstream in(cur);
in.imbue(std::locale(std::locale(), new commasep)); // for comma separation
std::tuple<Targs...> t;
read_tuple_elements(in, t, std::make_index_sequence<sizeof...(Targs)>{});
if (in) { // you may want to check if all data was consumed by adding && in.eof()
// now do something with the filled t;
}
else {
// the value could *not* successfully be read: somehow deal with that
}
}
以上代码的基本思想是简单地创建一个合适的调用序列 read_tuple_element()
。在进入通用代码之前,假设我们想要实现对仅包含三个元素的 std::tuple<T0, T1, T2> value
的读取。我们可以使用(为简洁起见,使用 rte()
而不是 read_tuple_element()
)来实现读取:
rte(get<0>(value)), rte(get<1>(value)), rte(get<2>(value));
现在,如果我们有一个索引序列std::size_t... I
,我们可以[几乎]使用
得到这个序列,而不是为每个元素写出这个序列
rte(get<I>(value))...;
但是不允许像这样扩展参数包。相反,需要将参数包放入某些上下文中。上面的代码为此目的使用了 std::initializer_list<bool>
:std::initializer_list<T>
的元素按列出的顺序构造。即我们得到了
std::initializer_list<bool>{ rte(get<I>(value))... };
缺少的部分是如何创建参数包 I
评估一系列合适的索引。方便的是,标准库定义了 std::make_index_sequence<Size>
,它创建了一个 std::index_sequence<I...>
,其中 I
的值序列为 0, 1, 2, ..., Size-1
。因此,用 std::make_index_sequence<sizeof...(Targs){}
调用 read_tuple_elements()
会创建一个对象,该对象具有合适的参数列表,可以推导出这些参数,然后用于将元组扩展为传递给 read_tuple_element()
.[=30 的元素序列=]
我正在开发一个简单的 CSV 解析器,它将文件的行存储在一个元组中。如果不是因为文件中各行的条目数及其类型都是变量,这将是一项简单的任务。因此,这些行可能是这样的:
1,2.2,你好,18,世界
解析器应该能够像这样工作:
ifstream file("input.csv");
SimpleCSVParser<int, float, string, int, string> parser(file);
当我尝试实现一个函数来解析实际行时,事情变得复杂了。我仍然没有找到一种方法来从参数列表中提取下一个类型以在调用 file >> var
之前声明变量。我还需要在循环中执行此操作,以某种方式从每次迭代的结果构建一个元组。
那么如何使用纯 C++11 将字符串解析为元组? 我试过这个:
template <typename ...Targs>
tuple<Targs...> SimpleCSVParser<Targs...>::iterator::operator*() {
istringstream in(cur);
in.imbue(locale(locale(), new commasep)); // for comma separation
tuple<Targs...> t;
for (size_t i = 0; i < sizeof...(Targs); ++i) {
tuple_element<i,decltype(t)>::type first;
in >> first;
auto newt = make_tuple(first);
// what do I do here?
}
}
但是它不起作用,因为我用来提取类型的元组是空的。
你不能像这样使用元组。
This would be an easy task if it wasn't for the fact that the number of entries on the lines inside the file is a variable, as well as their type.
据我了解,在处理文件时,您只在 运行 时知道您想要的元组大小和类型。不幸的是,这必须在编译时知道...
如果你真的想使用元组,你必须对你的文件进行预处理以确定数据大小和类型。然后您可以相应地使用正确的元组。但是你不能直接这样做。
for (size_t i = 0; i < sizeof...(Targs); ++i) {
tuple_element<i,decltype(t)>::type first;
in >> first;
auto newt = make_tuple(first);
// what do I do here?
}
这是运行时。您应该考虑使用可变参数模板的递归函数。那将是编译时间。
通过 type erasure 执行此类操作的常用方法,例如使用所有可能值类型的 union
加上指示哪个是实际条目的标志
namespace generic_type {
struct generic
{
enum type { Void=0, Bool=1, Int=2, String=3, Float=4 };
type Type=Void;
union {
bool B;
std::int64_t I;
std::string S;
double X;
}
generic() = default;
generic(generic&&) = default;
generic(generic const&) = default;
generic(bool b) : Type(Bool), B(b) {}
generic(std::int64_t i) : Type(Int), I(i) {}
generic(std::uint64_t i) : Type(Int), I(i) {}
generic(std::string const&s) : Type(String), S(s) {}
generic(std::string &&s) : Type(String), S(std::move(s)) {}
generic(double x) : Type(Float), X(x) {}
};
namespace details {// auxiliary stuff
template<typename T, typename E=void>
struct traits
{
static constexpr generic::type Type=generic::Void;
static void get(generic const&) {}
};
template<>
struct traits<bool,void>
{
static constexpr generic::type Type=generic::Bool;
static bool get(generic const&x) { return x.B; }
};
template<typename T>
struct traits<T,enable_if_t<std::is_integral<T>::value>
{
static constexpr generic::type Type=generic::Int;
static T get(generic const&x) { return x.I; }
};
template<>
struct traits<std::string,void>
{
static constexpr generic::type Type=generic::Str;
static std::string const& get(generic const&x) { return x.S; }
static std::string&& get(generic&&x) { return std::move(x.S); }
};
template<T>
struct traits<float,enable_if<std::is_same<T,float>::value ||
std::is_same<T,double>::value>
{
static constexpr generic::type Type=generic::Float; };
static T get(generic const&x) { return x.X; }
}
}
template<typename T>
auto unsafe_extract(generic const&x)
-> decltype(details::traits<T>::get(x))
{ return details::traits<T>::get(x); }
template<typename T>
auto unsafe_extract(generic&&x)
-> decltype(details::traits<T>::get(std::move(x)))
{ return details::traits<T>::get(std::move(x)); }
template<typename T>
auto extract(generic const&x)
-> decltype(unsafe_extract(x))
{
if(details::traits<T>::Type != x.Type)
throw std::runtime_error("type mismatch in extract(generic)");
return unsafe_extract(x);
}
template<typename T>
auto extract(generic&&x)
-> decltype(unsafe_extract(std::move(x)))
{
if(details::traits<T>::Type != x.Type)
throw std::runtime_error("type mismatch in extract(generic&&)");
return unsafe_extract(std::move(x));
}
}
using generic_type::generic;
然后您可以将数据存储在 std::vector<generic>
。
如果您使用 std::tuple_cat,您应该能够将每个后续值添加到元组中。如果我是你,我还建议使用 C++14 return 类型推导,这样就无需知道 return 类型。
看来,您尝试迭代元组 indices/types,我认为这不起作用。但是,您可以 做的是为每个成员调用一个读取函数。这个想法是将元组的处理委托给一个函数,该函数使用参数包将操作扩展为对每个元素的操作。 std::index_sequence<...>
可用于获取整数序列。
像这样:
template <typename T>
bool read_tuple_element(std::istream& in, T& value) {
in >> value;
return true;
}
template <typename Tuple, std::size_t... I>
void read_tuple_elements(std::istream& in, Tuple& value, std::index_sequence<I...>) {
std::initializer_list<bool>{ read_tuple_element(in, std::get<I>(value))... });
}
template <typename ...Targs>
tuple<Targs...> SimpleCSVParser<Targs...>::iterator::operator*() {
std::istringstream in(cur);
in.imbue(std::locale(std::locale(), new commasep)); // for comma separation
std::tuple<Targs...> t;
read_tuple_elements(in, t, std::make_index_sequence<sizeof...(Targs)>{});
if (in) { // you may want to check if all data was consumed by adding && in.eof()
// now do something with the filled t;
}
else {
// the value could *not* successfully be read: somehow deal with that
}
}
以上代码的基本思想是简单地创建一个合适的调用序列 read_tuple_element()
。在进入通用代码之前,假设我们想要实现对仅包含三个元素的 std::tuple<T0, T1, T2> value
的读取。我们可以使用(为简洁起见,使用 rte()
而不是 read_tuple_element()
)来实现读取:
rte(get<0>(value)), rte(get<1>(value)), rte(get<2>(value));
现在,如果我们有一个索引序列std::size_t... I
,我们可以[几乎]使用
rte(get<I>(value))...;
但是不允许像这样扩展参数包。相反,需要将参数包放入某些上下文中。上面的代码为此目的使用了 std::initializer_list<bool>
:std::initializer_list<T>
的元素按列出的顺序构造。即我们得到了
std::initializer_list<bool>{ rte(get<I>(value))... };
缺少的部分是如何创建参数包 I
评估一系列合适的索引。方便的是,标准库定义了 std::make_index_sequence<Size>
,它创建了一个 std::index_sequence<I...>
,其中 I
的值序列为 0, 1, 2, ..., Size-1
。因此,用 std::make_index_sequence<sizeof...(Targs){}
调用 read_tuple_elements()
会创建一个对象,该对象具有合适的参数列表,可以推导出这些参数,然后用于将元组扩展为传递给 read_tuple_element()
.[=30 的元素序列=]