如何直接从输入流中插入值?

Posted

技术标签:

【中文标题】如何直接从输入流中插入值?【英文标题】:How to insert values in set directly from input stream? 【发布时间】:2021-12-22 07:51:27 【问题描述】:

如何将输入从输入流直接插入到集合容器中?

这是我需要的

while(n--)

    cin>>s.emplace();

假设,我需要获取 n 个输入并将容器名称设置为 's'

while(n--)

    int x;
    cin>>x;
    s.emplace(x);

这很好,但我需要减少这一步。

【问题讨论】:

“我需要删掉这一步。”为什么?替代方案几乎肯定会被编译为有效完全相同的可执行文件。 代码行数是优化速度的糟糕指标。当你做任何不平凡的事情时,输入代码就像 10%(绝对最多)的编程时间。可读的代码将使您的速度提高几个数量级。 @Frank 更像 1% @Caleth 是的,我在这里过于保守/慷慨。 @Frank 这是 90% 的会议 :-) 【参考方案1】:

从 C++20 开始,您可以使用 std::ranges::copystd::counted_iteratorstd::istream_iteratorstd::default_sentinelstd::inserter 来执行此操作。 counted_iterator + default_sentinel 使它从流中复制 n 元素。

例子:

#include <algorithm> // ranges::copy
#include <iostream>
#include <iterator>  // counted_iterator, default_sentinel, istream_iterator, inserter
#include <set>
#include <sstream>   // istringstream - only used for the example

int main() 
    // just an example istream:
    std::istringstream cin("1 1 2 2 3 3 4 4 5 5");

    int n = 5;

    std::set<int> s;

    std::ranges::copy(
        std::counted_iterator(std::istream_iterator<int>(cin), n),
        std::default_sentinel,
        std::inserter(s, s.end())
    );

    for(auto v : s) std::cout << v << ' ';

输出将只包含 3 个元素,因为流中的前 5 个元素只有 3 个唯一元素:

1 2 3

在 C++20 之前,您可以以类似的方式使用 copy_n

std::copy_n(std::istream_iterator<int>(cin), n, std::inserter(s, s.begin()));

警告:如果流中的元素实际上少于 n,则两个版本都会有 undefined behavior。众所周知,流在交付您想要的内容时是不可预测的,copy_n 使错误检查变得非常困难。

为了安全起见,您可以创建一个counting_istream_iterator 以使用std::copy 从流中复制最多n 个元素,如下所示:

std::copy(counting_istream_iterator<foo>(cin, n),
          counting_istream_iterator<foo>,
          std::inserter(s, s.end()));

这样的迭代器可以基于std::istream_iterator,看起来像这样:

template<class T,
         class CharT = char,
         class Traits = std::char_traits<CharT>,
         class Distance = std::ptrdiff_t>
struct counting_istream_iterator 
    using difference_type = Distance;
    using value_type = T;
    using pointer = const T*;
    using reference = const T&;
    using iterator_category = std::input_iterator_tag;
    using char_type = CharT;
    using traits_type = Traits;
    using istream_type = std::basic_istream<CharT, Traits>;

    counting_istream_iterator() : // end iterator
        isp(nullptr), count(0) 

    counting_istream_iterator(std::basic_istream<CharT, Traits>& is, size_t n) :
        isp(&is), count(n + 1)
    
        ++*this; // read first value from stream
    
    counting_istream_iterator(const counting_istream_iterator&) = default;
    ~counting_istream_iterator() = default;

    reference operator*() const  return value; 
    pointer operator->() const  return &value; 

    counting_istream_iterator& operator++()         
        if(count > 1 && *isp >> value) --count;
        else count = 0; // we read the number we should, or extraction failed
        return *this;
    
    counting_istream_iterator operator++(int) 
        counting_istream_iterator cpy(*this);
        ++*this;
        return cpy;
    

    bool operator==(const counting_istream_iterator& rhs) const 
        return count == rhs.count;
    
    bool operator!=(const counting_istream_iterator& rhs) const 
        return !(*this == rhs);
    

private:
    std::basic_istream<CharT, Traits>* isp;
    size_t count;
    T value;
;

【讨论】:

这是问题的答案,但值得注意的是它有缺点。它使处理输入错误变得更加困难。如果cin 过早到达文件结束,您将获得未定义的行为。也无法处理错误输入,例如无法转换为int 的输入。这个解决方案简短而富有表现力,但否认任何实际的错误处理。我认为处理错误的唯一方法是为cin 启用异常:See this. 但这有其自身的问题。 要明确,我之前的评论不是对答案的批评。相反,我试图警告问题的目标可能不值得付出代价。 @FrançoisAndrieux 我很高兴你提到它。我的印象是,即使在早期的 EOF 出现时,这个组合也是安全的——但是,不,当我想到它时不是这样。确保安全需要额外的开销。 如果使用范围 std::istream_iterator&lt;int&gt;(cin)std::istream_iterator&lt;int&gt;() 是安全的,因为迭代器在到达 EOF 时将成为结束迭代器。这里只是不安全的,因为std::copy_n 至少假定n 元素并且从不检查end 迭代器。当您将 EOF 问题与读取n 元素的要求结合起来时,我认为没有一个标准算法可以解决这两个问题,我也会使用std::copy_n。它需要是一个自定义算法,可以同时接受范围和最大元素数。 @FrançoisAndrieux 我重写了这个答案。非常感谢您引起我的注意。又一个幻觉消失了。 :-)【参考方案2】:

您可以做的是创建自己的可重用函数,该函数提供您想要的简洁语法。你也可以把循环放在那里。

这样,您甚至可以进行正确的错误处理,同时保持主代码简洁明了。

一个完全通用的可能如下所示:

#include <iostream>
#include <set>
#include <stdexcept>

template<typename ContainerT>
void populate(ContainerT& container, std::istream& stream, std::size_t n) 
  using T = typename ContainerT::value_type;

  while(n--) 
    T tmp;
    if(stream >> tmp) 
      container.insert(container.end(), std::move(tmp));
    
    else 
      throw std::runtime_error("bad input");
    
  


int main() 
  std::size_t n = 5;

  std::set<int> s;
  populate(s, std::cin, n);  

你甚至可以变得更漂亮一点。例如,支持reserve() 的容器可以在开始填充容器之前调用它:

template<typename T>
concept HasReserve = requires(T x) 
   x.reserve(std::size_t);
;

// ...

ContainerT result;
if constexpr(HasReserve<ContainerT>) 
  container.reserve(container.size() + n);


//...


【讨论】:

以上是关于如何直接从输入流中插入值?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以从输入流中“丢弃”读取值?

从外部附件输入流中读取并推送到 s3 的最有效方法?

如何在张量流中张量的某些索引处插入某些值?

字节流和字符流

如何在从 NodeJS 中的多个输入流中读取时写入单个文件

从输入流中读取格式化文本[重复]