Description
While trying to make good use of the lambda syntax for pulling out results from a query, I hit an issue where the function is being passed by value (and thus copied), causing my results to get lost when the operator exited and the variable went out of scope.
In short, I want to create class instances for each result row, but also want to avoid code repetition. I currently have something similar to this:
// Note that this example is stripped down, and missing a lot of boilerplate.
class Route;
template<typename Target, typename... AttrTypes>
class Builder {
public:
std::vector<Target> results;
void operator()(AttrTypes... args) {
results.emplace_back(std::forward<AttrTypes&&>(args)...);
};
};
Builder<Route, std::string, std::string> route_builder;
class Route {
public:
std::string attribute1;
std::string attribute2;
Route(std::string a1, std::string a2) : attribute1(a1), attribute2(a2) {};
std::vector<Route> all(sqlite::database db) {
route_builder.results.empty();
db << "SELECT * FROM routes;"
>> route_builder;
return route_builder.results;
};
};
The relevant code for this issue is in Route::all()
at the end of the code block above, with the expression >> route_builder
. I am passing an instance of the Builder
class, which implements operator()
(as required by utility::function_traits
), and is thus runnable by the >>
implementation.
However, the current implementation takes the argument by value, meaning a local copy of the instance is created for use in the function, and any side effects are discarded when that variable falls out of scope (when operator>>
returns).
In other words, route_builder.results
- which I would expect to contain the Route
instances of each row returned from the query - is actually empty, because the operations were performed on an ephemeral copy of the instance.
My solution to this was to modify the Function
specialization of operator>>
to take its parameter by reference:
template <typename Function>
typename std::enable_if<!is_sqlite_value<Function>::value, void>::type operator>>(
Function& func) {
typedef utility::function_traits<Function> traits;
this->_extract([&func, this]() {
binder<traits::arity>::run(*this, func);
});
}
However, this does not work with the original, intended behavior of using a temporary lambda as the argument:
db << "SELECT route_short_name FROM route;" >> [&](std::string short_name) {
// This fails to compile, since this lambda is not an lvalue and therefore
// db::operator>> can not take a reference to it.
};
To get around this, a new specialization that takes an rvalue reference for these temporaries can be introduced. The resulting changes to the header would look like this:
template <typename Function>
typename std::enable_if<!is_sqlite_value<Function>::value, void>::type operator>>(
Function& func) {
...
}
template <typename Function>
typename std::enable_if<!is_sqlite_value<Function>::value, void>::type operator>>(
Function&& func) {
...
}
This works because lvalues will prefer matching to the lvalue reference specialization (the first one), and rvalue temporaries will prefer matching to the rvalue reference specialization.
I have tested this implementation with classes defining operator()
and direct lambdas, and both are working as of now. That said, though, I do not consider myself an expert in C++, so I'm not sure if this is a feasible solution.