Ключевое слово "yield" для С++, как вернуть Iterator из моей функции?
Рассмотрим следующий код.
std::vector<result_data> do_processing()
{
pqxx::result input_data = get_data_from_database();
return process_data(input_data);
}
std::vector<result_data> process_data(pqxx::result const & input_data)
{
std::vector<result_data> ret;
pqxx::result::const_iterator row;
for (row = input_data.begin(); row != inpupt_data.end(); ++row)
{
// somehow populate output vector
}
return ret;
}
Пока я думал о том, может ли я ожидать, что Оптимизация возвращаемого значения (RVO) произойдет, я нашел этот ответ Джерри Коффина [акцент мой]:
По крайней мере, ИМО, это обычно плохая идея, но не по соображениям эффективности. Это плохая идея, потому что рассматриваемая функция обычно должна быть написана как общий алгоритм , который выдает свой результат через итератор. Почти любой код, который принимает или возвращает контейнер вместо работы на итераторах, должен считаться подозрительным.
Не поймите меня неправильно: бывают времена, когда имеет смысл передавать объекты, подобные друг другу (например, строки), но в приведенном примере я бы рассмотрел передачу или возвращение вектора в плохую идею.
Имея некоторый фон Python, мне очень нравятся генераторы. На самом деле, если бы это был Python, я бы написал выше функцию в качестве генератора, т.е. Чтобы избежать необходимости обработки всех данных до того, как что-либо еще может произойти. Например, например:
def process_data(input_data):
for item in input_data:
# somehow process items
yield result_data
Если я правильно интерпретирую слова Джерри Коффина, это то, что он предложил, не так ли? Если да, как это реализовать на С++?
Ответы
Ответ 1
Нет, это не то, что Джерри означает, по крайней мере, не напрямую.
yield
в Python реализует сопрограммы. С++ не имеет их (но они, конечно, могут быть эмулированы, но это немного связано, если сделано чисто).
Но то, что имел в виду Джерри, - это просто, что вы должны передать выходной итератор, который затем записывается в:
template <typename O>
void process_data(pqxx::result const & input_data, O iter) {
for (row = input_data.begin(); row != inpupt_data.end(); ++row)
*iter++ = some_value;
}
И назовите его:
std::vector<result_data> result;
process_data(input, std::back_inserter(result));
Im не убежден, что это вообще лучше, чем просто возврат вектора.
Ответ 2
Существует сообщение в блоге автора Boost.Asio Chris Kohlhoff об этом: http://blog.think-async.com/2009/08/secret-sauce-revealed.html
Он моделирует yield
макросом
#define yield \
if ((_coro_value = __LINE__) == 0) \
{ \
case __LINE__: ; \
(void)&you_forgot_to_add_the_entry_label; \
} \
else \
for (bool _coro_bool = false;; \
_coro_bool = !_coro_bool) \
if (_coro_bool) \
goto bail_out_of_coroutine; \
else
Это должно использоваться в сочетании с классом coroutine
. Подробнее см. В блоге.
Ответ 3
Когда вы анализируете что-то рекурсивно или когда состояние обработки имеет состояние, шаблон генератора может быть хорошей идеей и значительно упростить код, поэтому невозможно выполнить итерацию, и обычно обратные вызовы являются альтернативой. Я хочу иметь yield
и найти, что Boost.Coroutine2, кажется, хорошо использовать сейчас.
Приведенный ниже код является примером для файлов cat
. Конечно, это бессмысленно до тех пор, пока вы еще не захотите обработать текстовые строки:
#include <fstream>
#include <functional>
#include <iostream>
#include <string>
#include <boost/coroutine2/all.hpp>
using namespace std;
typedef boost::coroutines2::coroutine<const string&> coro_t;
void cat(coro_t::push_type& yield, int argc, char* argv[])
{
for (int i = 1; i < argc; ++i) {
ifstream ifs(argv[i]);
for (;;) {
string line;
if (getline(ifs, line)) {
yield(line);
} else {
break;
}
}
}
}
int main(int argc, char* argv[])
{
using namespace std::placeholders;
coro_t::pull_type seq(
boost::coroutines2::fixedsize_stack(),
bind(cat, _1, argc, argv));
for (auto& line : seq) {
cout << line << endl;
}
}
Ответ 4
Я обнаружил, что подобное поведение похоже на то, что я имел в виду. Рассмотрим следующий (непроверенный) код:
struct data_source {
public:
// for delivering data items
data_source& operator>>(input_data_t & i) {
i = input_data.front();
input_data.pop_front();
return *this;
}
// for boolean evaluation
operator void*() { return input_data.empty() ? 0 : this; }
private:
std::deque<input_data_t> input_data;
// appends new data to private input_data
// potentially asynchronously
void get_data_from_database();
};
Теперь я могу сделать, как показывает следующий пример:
int main () {
data_source d;
input_data_t i;
while (d >> i) {
// somehow process items
result_data_t r(i);
cout << r << endl;
}
}
Таким образом, сбор данных каким-то образом отделен от обработки и, таким образом, может случиться лениво/асинхронно. То есть я мог обрабатывать элементы по мере их поступления, и мне не нужно ждать, пока вектор будет заполнен полностью, как в другом примере.