当我在C ++ 03上磨牙时,我学会了几种编写“给我收集东西”功能的方法。但每个都有一些挫折。
template< typename Container >
void make_collection( std::insert_iterator<Container> );
或:
void make_collection( std::vector<Thing> & );
或:
std::vector<Thing> make_collection();
使用现代C ++标准,是否有一个更为惯用的函数接口来“生成一个填充的容器”?
答案 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;。容器在函数中构造,然后通过移动语义移动,以避免复制。