-
Notifications
You must be signed in to change notification settings - Fork 156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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++; // iterate Maybe 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I greed :-) 👍
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
reference
the same asvalue_type
and return a newvalue_type
at every call to the operators.
If this is ever used with a not range for loop, everytime*iter
is used the read restarts at the first column.
So this could lead to very hard to find bugs. - Make the
value_type
member functionsconst
andvalue_type::next_index
mutable. Then a const reference can be returned.
This would work, but then we workconst
qualified instances of value_type, which don't act likeconst
objects 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zauguin If we drive from an std::tuple
, can we take advantage of C++17 structured bindings?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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_binder
behaves 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.