第一种方法是基于类型擦除的。
template<class T>
using sink = std::function<void(T&&)>;
Asink
是一个可调用的,它使用 的实例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>>
现在有一种完全不同的方法。与其编写集合生成器,不如编写生成器迭代器。
第一个是一个输入迭代器,它调用一些函数来生成项目并推进,最后一个是一个标记迭代器,当集合耗尽时与第一个迭代器比较。
该范围可以operator Container
使用 SFINAE 测试“它是否真的是一个容器”,或者.to_container<Container>
使用一对迭代器构建容器,或者最终用户可以手动进行。
These things are annoying to write, but Microsoft is proposing Resumable functions for C++ -- await and yield that make this kind of thing really easy to write. The generator<int>
returned probably still uses type erasure, but odds are there will be ways of avoiding it.
To understand what this approach would look like, examine how python generators work (or C# generators).
// 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());
note how fill_container
sort of looks like make_inserting_sink
turned upside down.
As noted above, the pattern of a generating iterator or range can be written manually without resumable functions, and without type erasure -- I've done it before. It is reasonably annoying to get right (write them as input iterators, even if you think you should get fancy), but doable.
boost
also has some helpers to write generating iterators that do not type erase and ranges.