Написание современного функционального интерфейса для создания заполненного контейнера

Когда я освоился в C++03, я изучил несколько подходов к написанию функции «дайте мне набор вещей». Но у каждого есть неудачи.

template< typename Container >
void make_collection( std::insert_iterator<Container> );

or:

void make_collection( std::vector<Thing> & );
  • Это не не зависит от контейнера
  • Интерфейс не сообщает, что ожидается пустой контейнер.

or:

std::vector<Thing> make_collection();
  • Это не не зависит от контейнера
  • Есть несколько способов ненужного копирования. (Неправильный тип контейнера, неправильный тип contained, отсутствие RVO, отсутствие семантики перемещения)

Используя современные стандарты С++, существует ли более идиоматический интерфейс функций для «создания заполненного контейнера»?


person Drew Dormann    schedule 05.03.2015    source источник
comment
Если вы хотите иметь возможность вставлять что-то в каждый контейнер, в том числе невидимый из файла .cpp, некоторый код должен быть виден в заголовке. Это единственное место, где есть информация как о целевом контейнере, так и о материалах, которые нужно поместить в контейнер. Чтобы отделить их, можно использовать стирание типов (поместить код генерации контента в файл .cpp и просто склеить код в заголовке), но это снижает эффективность во время выполнения. Каковы ваши приоритеты? Я имею в виду, вы можете попросить пирожные, которые останутся у вас в руке после того, как вы их проглотили, но вы можете их не получить.   -  person Yakk - Adam Nevraumont    schedule 05.03.2015
comment
Независимый от контейнера способ обработки вещей всегда заключался в использовании диапазона из пары итераторов. И, конечно же, всегда есть вставка адаптеров итераторов, но она больше не полностью независима от контейнера, используя их (поскольку они не могут справиться, например, с контейнерными адаптерами).   -  person Some programmer dude    schedule 05.03.2015
comment
@JoachimPileborg, который не поддерживает добавление элементов?   -  person Yakk - Adam Nevraumont    schedule 05.03.2015


Ответы (4)


Первый подход основан на стирании типа.

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
comment
Придирка: разве std::inserter не должно быть std::back_inserter? Вопрос: не будет ли проблем с записью захвата лямбды [&c] и просто использованием c внутри? - person bogdan; 05.03.2015
comment
@bogdan std::back_inserter работает только с контейнерами, которые предоставляют push_back, поэтому std::inserter немного более общий (подумайте std::map и std::unordered_map). Захват ссылок по ссылке в настоящее время недостаточно определен (см. выпуск CWG 2011 г. ). - person Casey; 05.03.2015
comment
@Casey Верное указание на std::inserter как на способ заставить его работать с большим количеством контейнеров, хотя я не уверен, что это хорошая идея. Я даже не думал о картах и ​​наборах, используемых с этим кодом, поскольку добавление к ним элементов достаточно отличается, чтобы гарантировать отдельную реализацию (я предполагаю, что постоянное использование end() в качестве подсказки для вставки в карту может быть хуже чем использование insert вообще без подсказки, и причин для неудачной вставки больше, чем, скажем, для вектора). - person bogdan; 05.03.2015
comment
@Casey Спасибо за ссылку на проблему. Знаете ли вы какой-нибудь компилятор, который не реализует захват по ссылке, как указано в предлагаемом решении? Я имею в виду, что у вас не может быть ссылок на ссылки в C++. Конечно, здорово, что формулировка уточняется, но кто-нибудь сталкивался с проблемами в реальном коде из-за этого? - person bogdan; 05.03.2015
comment
@bogdan Я не знаю такого компилятора и был бы так же удивлен, как и вы, компилятором, который не собирал ссылки по ссылке именно таким образом. Если бы я писал это, я бы, вероятно, полностью обошел проблему, зафиксировав результат std::inserter(c, end(c)) и сделав лямбду mutable. Решение Yakk имеет то преимущество, что всегда вставляется в конец контейнера, даже если другие модификации делают недействительными итераторы между вызовами лямбда-выражения. - person Casey; 05.03.2015
comment
@Bogdan Итак, существует чрезвычайно эффективная реализация захвата [&], которая просто захватывает указатель кадра стека и использует его для доступа к переменным в контексте, где определена лямбда. Это делает вашу ссылку, захватывающую лямбды размером sizeof(void*), и должно быть столь же эффективным, как и предлагаемое решение, но оно ломается, когда вы захватываете ссылку на ссылку, а внешняя ссылка выходит за рамки. Учитывая эту оптимизацию, я буду кодировать консервативно. - person Yakk - Adam Nevraumont; 06.03.2015
comment
@Casey, спасибо - это то, что я получаю за то, что не компилировал свой ответ. Я заменил c.end() на end(c), чтобы включить поиск ADL. Также аналогичным образом исправлено inserter (я могу вспомнить ситуации, когда может потребоваться вызов inserter на основе ADL) - person Yakk - Adam Nevraumont; 06.03.2015
comment
@Yakk Знаете ли вы какой-либо компилятор, который таким образом реализует захват по ссылке? Потому что, если бы этого не произошло до сих пор, я бы сказал, что маловероятно, что какая-либо реализация начнет использовать эту оптимизацию в будущем, учитывая, что в отношении этого есть проблема в статусе проверки. В качестве побочного примечания вы можете захватить указатель на объект, на который указывает ссылка, путем набора текста без копирования :-). - person bogdan; 06.03.2015
comment
@template Не знаю. Выучить Python и Haskell? - person Yakk - Adam Nevraumont; 06.03.2015
comment
@bogdan нет, но я не знаю, куда пойдет этот дефект. Его нет в черновом стандарте. - person Yakk - Adam Nevraumont; 06.03.2015
comment
@Yakk Действительно, это не так, но, насколько я понимаю, он почти готов к представлению на утверждение всем комитетом. Учитывая, что он указывает что-то, что согласуется с обработкой ссылок во всем остальном языке, я возлагаю большие надежды... - person bogdan; 06.03.2015
comment
@bog, и я думаю, оптимизация все еще работает для не-ссылок, захваченных по ссылке - person Yakk - Adam Nevraumont; 06.03.2015
comment
@Yakk Верно. Также верно для ссылок на локальные объекты и объекты параметров, где «следующая» ссылка останется внутри текущего кадра стека. Только ссылки, которые указывают «снаружи», нуждаются в своем собственном члене в типе закрытия; это занимает место, но делает использование этих ссылок быстрее (в более простом варианте, когда для всего используется только указатель кадра, доступ к ссылке, захваченной ссылкой, потребует еще одной косвенности). - person bogdan; 06.03.2015

Если мы черпаем вдохновение из стандарта, почти все в форме 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 для заполнения с нуля и т. д. Единственное, что вам нужно сделать, это предоставить соответствующий генератор. Имя даже указывает на то, что он ожидает, что контейнер будет пустым, если вы используете итераторы в стиле вставки.

person Mark B    schedule 05.03.2015

Вы можете сделать это в С++ 11 без копирования контейнера. Вместо конструктора копирования будет использоваться конструктор перемещения.

std::vector<Thing> make_collection()
person Bla Bla    schedule 05.03.2015
comment
В C++03 RVO и NRVO также могут привести к исчезновению копий. C++03 просто гарантирует, что если RVO/NRVO не будет применено, то он переместится. - person Yakk - Adam Nevraumont; 05.03.2015
comment
@Yakk, ты не имеешь в виду гарантии С++ 11? - person Mgetz; 05.03.2015
comment
@Mgetz да, C++03 не гарантирует move, C++11 гарантирует. Упс. - person Yakk - Adam Nevraumont; 05.03.2015

Я не думаю, что существует один идиоматический интерфейс для создания заполненного контейнера, но похоже, что в этом случае вам просто нужна функция для создания и возврата контейнера. В этом случае вы должны предпочесть последний случай:

std::vector<Thing> make_collection();

Этот подход не приведет к "ненужному копированию", если вы используете современный C++11-совместимый компилятор. Контейнер создается в функции, а затем перемещается с помощью семантики перемещения, чтобы избежать копирования.

person Julian McKinlay    schedule 05.03.2015
comment
Упоминание об элизии улучшит этот ответ. - person Yakk - Adam Nevraumont; 05.03.2015
comment
Насколько я понимаю, исключение копирования было частью оптимизации возвращаемого значения, которую современные компиляторы применяли для предотвращения копирования временного объекта, и что в С++ 11 эта оптимизация по существу заменена семантикой перемещения. Или компилятор теперь пропускает присваивание перемещения? - person Julian McKinlay; 05.03.2015