编写现代函数接口以"produce a populated container"
Writing a modern function interface to "produce a populated container"
当我在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兼容的现代编译器,这种方法就不会产生任何"不必要的复制"。容器在函数中构造,然后通过移动语义移动,以避免复制。