Первый подход основан на стирании типа.
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
, а возвращаемая лямбда-выражение принимало бы 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
с захватом присваивания является конструкцией C++14, но может быть реализовано как объект неанонимной вспомогательной функции в C++03.
Мы также могли бы оптимизировать make_collection
для чего-то вроде std::vector<int>
с небольшой диспетчеризацией тегов (таким образом, make_collection<std::vector<int>>
позволит избежать накладных расходов на стирание типа).
Сейчас совсем другой подход. Вместо того, чтобы писать генератор коллекций, напишите итераторы генератора.
Первый — это входной итератор, который вызывает некоторые функции для создания элементов и продвижения вперед, последний — сигнальный итератор, который сравнивается с первым, когда коллекция исчерпана.
Диапазон может иметь operator Container
с проверкой SFINAE на предмет "действительно ли это контейнер" или .to_container<Container>
, который создает контейнер с парой итераторов, или конечный пользователь может сделать это вручную.
Такие вещи неприятно писать, но Microsoft предлагает возобновляемые функции для C++ -- await и 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
также имеет несколько помощников для написания генерирующих итераторов, которые не вводят стирание и диапазоны.
person
Yakk - Adam Nevraumont
schedule
05.03.2015