Reputation: 210
I've implemented a little helper wrapper for range-for loops that allows iterating over both keys and values of the associative Qt containers like QMap
and QHash
, extracting each pair as a structured binding, e.g.:
const QMap<int, QString> digitMap = { {1, "one"}, {2, "two"}, {3, "three"} };
for (auto [intKey, strVal] : make_keyval(digitMap)) {
qDebug() << intKey << "->" << strVal;
}
This isn't supported out-of-the-box for the Qt containers as they require using a specific pair of constKeyValueBegin()
and constKeyValueEnd()
methods (let's assume only non-mutating iterations). So my idea was to write a simple wrapper type that provides a pair of regular begin()
and end()
methods which simply call the keyValue ones on the container.
That's easy enough, but as a stretch goal I also wanted to make the wrapper usable with temporaries, like make_keyval(sometype.toMap())
. The main challenge there was extending the lifetime of the temporary through the end of the iteration, as I'm going through a proxy object. Here's the solution I came up with:
template<typename C>
struct key_value_range_iterator {
key_value_range_iterator(const C& container) : m_rvalueContainer(nullptr), m_containerRef(container) {}
key_value_range_iterator(const C&& container) : m_rvalueContainer(std::make_unique<C>(std::move(container))), m_containerRef(*m_rvalueContainer) {}
typename C::const_key_value_iterator begin() const { return m_containerRef.constKeyValueBegin(); }
typename C::const_key_value_iterator end() const { return m_containerRef.constKeyValueEnd(); }
private:
const std::unique_ptr<C> m_rvalueContainer;
const C& m_containerRef;
};
template<typename C>
auto make_keyval(C&& container) { return key_value_range_iterator(std::forward<C>(container)); }
This seems to work fine for both regular variables and temporaries. For temporaries, m_rvalueContainer
is used to store the moved temporary for the duration of the iteration, then it's referenced by m_containerRef
. In the regular variable case, we just store the lvalue reference in m_containerRef
directly leaving m_rvalueContainer
unset. I verified the right constructor gets called in each case, and that the temporary only gets destroyed after the range-for loop completes.
So my question is simply: is this a correct implementation for my wrapper, or did I miss something? Or maybe there's a corner case I haven't thought of?
Note that in my initial version, I had m_rvalueContainer
as a regular value, but I figured this would end up instantiating an empty container for nothing in the lvalue case (though that's a cheap operation for Qt containers), so I replaced it with unique_ptr
to ensure there's no overhead in that case. Yes, it still ends up initializing it to nullptr, but that's neglectable.
Any other comments or recommendations?
Upvotes: 1
Views: 272
Reputation: 12968
Since make_keyval
knows if the object passed in is lvalue or rvalue, you can pass that parameter on to your wrapper.
#include <type_traits>
template<typename C>
struct key_value_range_iterator {
key_value_range_iterator(const C container) : m_containerRef(container) {}
using iterator = typename std::remove_reference_t<C>::const_key_value_iterator;
iterator begin() const { return m_containerRef.constKeyValueBegin(); }
iterator end() const { return m_containerRef.constKeyValueEnd(); }
private:
const C m_containerRef;
};
template<typename C>
auto make_keyval(C&& container) { return key_value_range_iterator<C>(std::forward<C>(container)); }
When passing an lvalue C
is deduces as QMap&
making the wrapper hold a reference.
When passing an rvalue C
is deduces as QMap
making the wrapper move the rvalue into it's member.
Since C
can be QMap&
we need to use std::remove_reference
to obtain the iterator type succesfully for the lvalue case.
Upvotes: 3