将C++字符串解析为元组

5

我正在开发一个简单的CSV解析器,它可以将文件中的行存储在元组中。如果文件内每行的条目数量和类型都是固定的,那这将是一个简单的任务。但实际上,文件内每行的条目数量和类型都是不确定的。因此,文件内的行可能会像这样:

1,2.2,hello,18,world

解析器应该能够像这样工作:

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

但是由于我用于提取类型的元组为空,所以它不起作用。


请展示你不能正常运行的解析器。 - Roberto
@Roberto 更新了帖子。 - Dmitry Serov
tuple_element<i,decltype(t)> 需要一个 constexpri,而不是一个运行时变量。 - Walter
5个回答

4

看起来你试图迭代不起作用的元组索引/类型,我想。然而,你可以为每个成员调用一个读取函数。思路是将元组的处理委托给一个使用参数包来展开对每个元素的操作的函数。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>值。我们可以使用以下方式来实现读取(使用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 ,它创建一个std :: index_sequence ,其中包含I的值序列为0,1,2,...,Size-1。因此,使用std :: make_index_sequence 调用read_tuple_elements()将创建一个具有适当参数列表的对象,该参数列表可以被推断并用于展开元组成为传递给read_tuple_element()的元素序列。

以下代码行无法编译,报错信息为“表达式包含未展开的参数包”:dummy(read_tuple_element(in, std::get<I>(value))); - Dmitry Serov
std::get<I>(value))...); 后面添加 ... 确实修复了错误。但我仍然不完全理解为什么我们需要这个虚拟函数。您能否简要解释一下它的目的? - Dmitry Serov
谢谢!这段代码确实做到了我想要的。现在我猜我会尝试弄清楚它是如何以及为什么起作用的。 - Dmitry Serov
@DmitrySerov:好的,我已经添加了一些解释(我很快就会从这个答案中删除评论,以避免它看起来凌乱...)。 - Dietmar Kühl

2

您不能这样使用元组。

如果文件内部行的条目数以及它们的类型是变量,那么这将是一个简单的任务。

如果我理解正确,您只在处理文件时运行时才知道所需元组的大小和类型。不幸的是,这必须在编译时知道......

如果您真的想使用元组,则必须对文件进行预处理以确定数据大小和类型。然后您可以相应地使用正确的元组。但是您无法直接这样做。


1
通常执行此类操作的方法是通过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> 中。


0
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?
}

这是运行时。你应该考虑使用可变模板递归函数。那将是编译时。


0
如果你使用 std::tuple_cat ,你应该能够将每个后续值添加到元组中。如果我是你,我也会建议使用 C++14 返回类型推导,这样可以省去需要知道返回类型的步骤。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接