Consider the following code.
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;
}
While I was thinking about whether or not I could expect Return Value Optimization (RVO) to happen, I found this answer by Jerry Coffin [emphasis mine]:
At least IMO, it's usually a poor idea, but not for efficiency reasons. It's a poor idea because the function in question should usually be written as a generic algorithm that produces its output via an iterator. Almost any code that accepts or returns a container instead of operating on iterators should be considered suspect.
Don't get me wrong: there are times it makes sense to pass around collection-like objects (e.g., strings) but for the example cited, I'd consider passing or returning the vector a poor idea.
Having some Python background, I like Generators very much. Actually, if it were Python, I would have written above function as a Generator, i.e. to avoid the necessity of processing the entire data before anything else could happen. For example like this:
def process_data(input_data):
for item in input_data:
# somehow process items
yield result_data
If I correctly interpreted Jerry Coffins note, this is what he suggested, isn't it? If so, how can I implement this in C++?
Each iteration of the foreach loop calls the iterator method. When a yield return statement is reached in the iterator method, expression is returned, and the current location in code is retained. Execution is restarted from that location the next time that the iterator function is called.
yield provides an easy way of implementing the iterator protocol, defined by the following two methods: __iter__ and next (Python 2) or __next__ (Python 3). Both of those methods make an object an iterator that you could type-check with the Iterator Abstract Base Class from the collections module.
In the iterator block, the yield keyword is used together with the return keyword to provide a value to the enumerator object. This is the value that is returned, for example, in each loop of a foreach statement. The yield keyword is also used with break to signal the end of iteration."
The yield keyword is use to do custom stateful iteration over a collection. The yield keyword tells the compiler that the method in which it appears is an iterator block. yield return <expression>; yield break; The yield return statement returns one element at a time.
No, that’s not what Jerry means, at least not directly.
yield
in Python implements coroutines. C++ doesn’t have them (but they can of course be emulated but that’s a bit involved if done cleanly).
But what Jerry meant is simply that you should pass in an output iterator which is then written to:
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;
}
And call it:
std::vector<result_data> result;
process_data(input, std::back_inserter(result));
I’m not convinced though that this is generally better than just returning the vector.
There is a blog post by Boost.Asio author Chris Kohlhoff about this: http://blog.think-async.com/2009/08/secret-sauce-revealed.html
He simulates yield
with a macro
#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
This has to be used in conjunction with a coroutine
class. See the blog for more details.
When you parse something recursively or when the processing has states, the generator pattern could be a good idea and simplify the code greatly—one cannot easily iterate then, and normally callbacks are the alternative. I want to have yield
, and find that Boost.Coroutine2 seems good to use now.
The code below is an example to cat
files. Of course it is meaningless, until the point when you want to process the text lines further:
#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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With