编写一个现代功能界面来“生成一个填充的容器”

时间:2015-03-05 18:07:02

标签: c++ c++11 c++14 c++17

当我在C ++ 03上磨牙时,我学会了几种编写“给我收集东西”功能的方法。但每个都有一些挫折。

template< typename Container >
void make_collection( std::insert_iterator<Container> );

或:

void make_collection( std::vector<Thing> & );
  • 这是容器不可知
  • 接口不会传达预期的空容器。

或:

std::vector<Thing> make_collection();
  • 这是容器不可知
  • 有多种途径可以进行不必要的复制。 (错误的容器类型,错误的包含类型,没有RVO,没有移动语义)

使用现代C ++标准,是否有一个更为惯用的函数接口来“生成一个填充的容器”?

4 个答案:

答案 0 :(得分:7)

第一种方法是基于类型擦除。

template<class T>
using sink =  std::function<void(T&&)>;

sink是一个使用T实例的可调用对象。数据流入,没有任何流出(调用者可见)。

template<class Container>
auto make_inserting_sink( Container& c ) {
  using std::end; using std::inserter;
  return [c = std::ref(c)](auto&& e) {
    *inserter(c.get(), end(c.get()))++ = decltype(e)(e);
  };
}

make_inserting_sink接受一个容器,并生成一个消耗要插入的东西的sink。在一个完美的世界中,它将是make_emplacing_sink并且返回的lambda将采用auto&&...,但我们为我们拥有的标准库编写代码,而不是我们希望拥有的标准库。

以上两者都是通用库代码。

在收藏代的标题中,您有两个功能。一个template粘合函数,以及一个完成实际工作的非模板函数:

namespace impl {
  void populate_collection( sink<int> );
}
template<class Container>
Container make_collection() {
  Container c;
  impl::populate_collection( make_inserting_sink(c) );
  return c;
}

您在头文件之外实现impl::populate_collection,它只是一次将一个元素移交给sink<int>。请求的容器与生成的数据之间的连接由sink删除。

以上假设您的收藏集是int的集合。只需更改传递给sink的类型,即可使用其他类型。生成的集合不必是int的集合,只需要int作为其插入迭代器的输入。

这不是完全有效的,因为类型擦除会产生几乎不可避免的运行时开销。如果您将void populate_collection( sink<int> )替换为template<class F> void populate_collection(F&&)并在头文件中实现,则类型擦除开销就会消失。

std::function是C ++ 11的新功能,但可以在C ++ 03或之前实现。具有赋值捕获的auto lambda是C ++ 14构造,但可以在C ++ 03中实现为非匿名辅助函数对象。

我们还可以通过一些标记调度来优化make_collection std::vector<int>之类的内容(因此make_collection<std::vector<int>>会避免类型擦除开销)。


现在有一种完全不同的方法。而不是编写集合生成器,编写生成器迭代器。

第一个是一个输入迭代器,它调用一些函数来生成项目并前进,最后一个是一个sentinal迭代器,它与集合中的第一个迭代器进行比较。

范围可以有operator Container SFINAE测试“它真的是一个容器”,或者.to_container<Container>用一对迭代器构造容器,或者最终用户可以手动执行

写这些东西很烦人,但Microsoft is proposing Resumable functions for C++ -- await and yield使得这种东西很容易写。返回的generator<int>可能仍然使用类型擦除,但可能会有避免它的方法。

要了解这种方法的样子,请检查python生成器的工作方式(或C#生成器)。

// exposed in header, implemented in cpp
generator<int> get_collection() resumable {
  yield 7; // well, actually do work in here
  yield 3; // not just return a set of stuff
  yield 2; // by return I mean yield
}
// I have not looked deeply into it, but maybe the above
// can be done *without* type erasure somehow.  Maybe not,
// as yield is magic akin to lambda.

// This takes an iterable `G&& g` and uses it to fill
// a container.  In an optimal library-class version
// I'd have a SFINAE `try_reserve(c, size_at_least(g))`
// call in there, where `size_at_least` means "if there is
// a cheap way to get the size of g, do it, otherwise return
// 0" and `try_reserve` means "here is a guess asto how big
// you should be, if useful please use it".
template<class Container, class G>
Container fill_container( G&& g ) {
  Container c;
  using std::end;
  for(auto&& x:std::forward<G>(g) ) {
    *std::inserter( c, end(c) ) = decltype(x)(x);
  }
  return c;
}
auto v = fill_container<std::vector<int>>(get_collection());
auto s = fill_container<std::set<int>>(get_collection());

请注意fill_container看起来像make_inserting_sink的内容是如何颠倒过来的。

如上所述,生成迭代器或范围的模式可以手动编写而无需可恢复的函数,并且没有类型擦除 - 我之前已经完成了。正确起来是相当烦人的(将它们写成输入迭代器,即使你认为你应该得到它的想象力),但是可行。

boost还有一些帮助程序可以编写生成迭代器而不键入erase和range。

答案 1 :(得分:2)

如果我们从标准中获取灵感,那么make_<thing>形式的任何内容都将按值返回<thing>(除非分析表明否则我不相信按值返回应该排除逻辑方法)。这表明选项三。如果您希望提供一些容器灵活性,您可以将其设为模板模板(您只需要了解允许的容器是否关联)。

但是,根据您的需求,您是否考虑过从std::generate_n获取灵感而不是制作容器,而是提供fill_container功能?然后它看起来与std::generate_n非常相似,类似于

template <class OutputIterator, class Generator>
void fill_container (OutputIterator first, Generator gen);

然后,您可以替换现有容器中的元素,也可以使用insert_iterator从头开始填充等。您唯一需要做的就是提供适当的生成器。该名称甚至表示如果您使用插入式迭代器,它希望容器为空。

答案 2 :(得分:0)

您可以在没有容器复制的情况下在c ++ 11中执行此操作。将使用移动构造函数而不是复制构造函数。

std::vector<Thing> make_collection()

答案 3 :(得分:0)

我不认为有一个惯用接口来生成一个填充的容器,但听起来在这种情况下你只需要一个函数来构造和返回一个容器。在这种情况下,你应该更喜欢你的最后一个案例:

std::vector<Thing> make_collection();

只要您使用的是现代C ++ 11兼容编译器,这种方法就不会产生任何不必要的复制&#34;。容器在函数中构造,然后通过移动语义移动,以避免复制。