Alternative iterator interface#126
Conversation
|
I like it more then #103 it looks more consistent with the rest of the API. But now that i think about it, isnt it a lot like the prepared statement stuff? auto prep = db << "select... where ?"; // create
prep << 21; // bind
auto itr = prep.begin() // "execute" // till here the prep statement does the same
auto row = itr++; // iterateMaybe there is some synergy that could be used? |
|
I added some documentation and rebased it on the current master. If noone objects I would merge it soon. |
|
I'm totaly out the loop here. Didnt touch C++ for the last 10+ Month. So this look like magic now ;) |
| } | ||
| template<class ...Types> | ||
| operator std::tuple<Types...>() { | ||
| std::tuple<Types...> value; |
There was a problem hiding this comment.
@zauguin This constructs an empty tuple<Types...> and then assigns to each tuple elements.
Can we avoid the overhead?
There was a problem hiding this comment.
The problem is that our interface always gets a referrence to a preexisting object.
So fixing this would require to rewrite get_col_from_db for every type to return the value instead of storing it in a parameter.
This is a streight forward conversion but I think it would be better to do this in a separate Pull Request because it would add another 100+ line of changes, so reviewing this together could easiliy become be a pain.
Also the overhead should be small or nonexistent: The values are default constructed, then in a probably inlined function a value is assigned, so most optimizers should eliminate the additional construction anyway.
hdr/sqlite_modern_cpp.h
Outdated
| tuple_iterate<std::tuple<Types...>>::iterate(values, *this); | ||
| }); | ||
| // Not well-defined | ||
| row_iterator operator++(int); |
There was a problem hiding this comment.
@zauguin I think we should not declare the postfix operator, a compile error is better than a linker one.
There was a problem hiding this comment.
@aminroosta This was added to be compatible with the Ranges TS.
The operator++ will normally not be necessary, but it is checked for with concepts.
But this was a hack in the first place and I agree moving a compile-time to a link-time error is bad.
We can either just take it out. This is the nice way and we don't lie about our type. The downside would themissing compatibility with Ranges-v3 or the Ranges TS.
We could also rewrite it to fail at compile time iff we are in an evaluated context, so using it would lead to a compile-time error, but checking for it would show it's existance.
Obviously this would really be a ugly hack, but we could interact with most libraries expecting "real iterators".
There was a problem hiding this comment.
We can either just take it out. This is the nice way and we don't lie about our type. The downside would themissing compatibility with Ranges-v3 or the Ranges TS.
I think we should do that, unless we can come up with so called "real iterators" :-)
| typedef utility::function_traits<Function> traits; | ||
| private: | ||
| database_binder *_binder = nullptr; | ||
| mutable value_type value{_binder}; // mutable, because `changing` the value is just reading it |
There was a problem hiding this comment.
@zauguin I'm just wondering since we have no const member function modifying the value, why do we need mark it as mutable?
There was a problem hiding this comment.
If this is not mutable, operator* has to return a const reference to value_type instead.
But reading from the value_type requires increasing the index for the next column, next_index.
I see four ways to solve this:
- Make
operator *andoperator ->not const.
This breaks requirements for iterators and might break code. IMO it would also be weird, because these functions are not changing anything. - Make
referencethe same asvalue_typeand return a newvalue_typeat every call to the operators.
If this is ever used with a not range for loop, everytime*iteris used the read restarts at the first column.
So this could lead to very hard to find bugs. - Make the
value_typemember functionsconstandvalue_type::next_indexmutable. Then a const reference can be returned.
This would work, but then we workconstqualified instances of value_type, which don't act likeconstobjects at all because they change if any function on them is called. - The current approach: A mutable
value.
This is certainly not perfect, but it moves the problems of 3. to the iterator type which is less visible to the user.
There was a problem hiding this comment.
@zauguin Thanks for the clarification, Since value_type does not own the underlying data, I too think solution No 4 is the right choice here.
Right now value_type will only read data if dereferenced (*value) or when it is implicitly converted to std::tuple, basically a non-owing type.
What are your thoughts if we make it own the entire row?
value_type can own an std::tuple member, or drive from std::tuple<Types...>
We will initialize & update the underlying tuple in row_iterator::operator++.
There was a problem hiding this comment.
@zauguin If we drive from an std::tuple, can we take advantage of C++17 structured bindings?
There was a problem hiding this comment.
@aminroosta This sounds like the interface of #103.
The fundamental problem is determining the type and number of columns at compile time. If the value_type should own the values, the value_type has to know about <Types...>, so the types have to be passed as explicit template arguments.
There was a problem hiding this comment.
@aminroosta How do you prefere to move forward here? Do you prefer the interface opions discussed in #103 or should we continue with this one?
|
@zauguin Thanks for the really great work, This PR looks really interesting. I've added a review, But I think it looks good to merge even in its current shape. |
|
@zauguin A quick discussion about the branches we are using. I will propose, we should have a This way can have an experimental( |
|
@aminroosta This sounds like a great plan. Will you create the branch and set everything up? |
|
@zauguin Great, I will try to setup that by the end of this week. I am planning to do the following:
|
|
@aminroosta i'm not sure what these branches do, so they are probably not important. |
Alternative iterator interface
I think this feels more natural than the interface from #103 and it makes lifetime questions easier.
The
database_binderbehaves like a container now:@Killili What do you think? It's not our usual stream-op interface, but I think this makes it clearer that the range doesn't exists independent from the statement.