编写现代函数接口以"produce a populated container"

Writing a modern function interface to "produce a populated container"

本文关键字:produce populated container 接口 函数      更新时间:2023-10-16

当我在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++标准,是否有更惯用的函数接口来"生成已填充的容器"?

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

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作为其插入迭代器的输入的集合。

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

std::function是C++11中的新功能,但可以在C++03或更早版本中实现。带有赋值捕获的auto lambda是一个C++14构造,但可以在C++03中作为非匿名辅助函数对象实现。

我们还可以通过一点标签调度来优化make_collection,以实现类似std::vector<int>的功能(因此make_collection<std::vector<int>>将避免类型擦除开销)。


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

第一个是一个输入迭代器,它调用一些函数来生成项并前进,最后一个是一种重要迭代器。当集合被丢弃时,它与第一个迭代器进行比较。

该范围可以有一个带有SFINAE测试的operator Container来测试"它真的是一个容器吗",或者一个用一对迭代器构建容器的.to_container<Container>,或者最终用户可以手动完成。

这些东西写起来很烦人,但微软提出了C++的可恢复函数--wait和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也有一些帮助程序来编写生成迭代器,这些迭代器不键入擦除和范围。

如果我们从标准中获得灵感,那么几乎任何形式的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从头开始填充,等等。您唯一需要做的就是提供适当的生成器。如果您使用插入式迭代器,名称甚至表示它希望容器为空。

您可以在c++11中做到这一点,而无需复制容器。将使用移动构造函数而不是复制构造函数。

std::vector<Thing> make_collection()

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

std::vector<Thing> make_collection();

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