Containers with C++17 structured bindings



  • Since C++ 17, structured bindings exist in the language. These are very useful for iterating over maps for example.
    This works fine for std::map, but unfortunately does not work for Qt Containers like QMap.
    Is there a plan to add this in and/or a way to make it work in a similar way?
    Or should I rather use plain iterators instead?

    Example code:

    #include <QString>
    #include <QMap>
    #include <map>
    #include <QDebug>
    
    int main()
    {
    	const QMap<int, QString> qt_m =
    	{
    		{21, "hello"},
    		{684, "second"},
    		{831, "end"}
    	};
    	const auto std_m = qt_m.toStdMap();
    
    	//structured bindings to iterate over std::map. works fine
    	for( const auto& [key, value] : std_m )
    	{
    		qDebug() << key << " : " << value << '\n';
    	}
    
    	//structured bindings to iterate over QMap. does not compile
    	for( const auto& [key, value] : qt_m )
    	{
    		qDebug() << key << " : " << value << '\n';
    	}
     }
    
    

  • Qt Champions 2018

    @david_dlqa said in Containers with C++17 structured bindings:

    Is there a plan to add this in and/or a way to make it work in a similar way?

    Hi and welcome!

    This is more a question for Qt developers (on their mailing list). Here we are in a user forum.



  • Hi, thanks for letting me know.
    I guess the question then is, if plain iterators are the best way to do this if I need both the key and value in a loop.


  • Moderators

    Unlike std::map Qt does not store the key/value in a pair, so you can't bind your structure to the result of operator*. It returns only the value.

    If you need both key and a value you can either use the map's iterator:

    for( auto it = qt_m.cbegin(); it != qt_m.cend(); ++it )
    {
        qDebug() << it.key() << " : " << it.value() << '\n';
    }
    

    or the iterator class:

    QMapIterator it(qt_m);
    while(it.hasNext())
    {
        it.next();
        qDebug() << it.key() << " : " << it.value() << '\n';
    }
    


  • Or:

    for(auto key: qt_m.keys()){
      qInfo() << key << qt_m[key];
    }
    

  • Moderators

    @fcarney Yeah, but that's a very bad solution. First keys() makes a copy of the data into a new container and then you make a find for each element (that's what operator[] is essentially). It's terrible both performance wise and for memory consumption. Please don't do this in production code.



  • @Chris-Kawa said in Containers with C++17 structured bindings:

    It's terrible both performance wise and for memory consumption.

    Now I can see why keys() is not in the std::map. It would encourage poor coding practices. Thanks for your insight. I generally use this to see whats in my map for debugging, but yeah I can see the performance problems it would create on large datasets.


  • Lifetime Qt Champion


  • Moderators

    @SGaist Huh, that's cool. I didn't know those existed. Thanks!

    @david_dlqa So you could write a little adapter thingy like this:

    template<class Container> class adapt
    {
    public:
        adapt(const Container& container) : c(container) {}
        typename Container::const_key_value_iterator begin() const { return c.keyValueBegin(); }
        typename Container::const_key_value_iterator end() const { return c.keyValueEnd(); }
    private:
        const Container& c;
    };
    

    and then you could use structured bindings like so:

    for( const auto& [key, value] : adapt(qt_m) )
    {
        qDebug() << key << " : " << value << '\n';
    }
    

    From what i see it's still an inferior solution performance wise, as those keyValue iterators make pairs on the fly, but it might be good enough.


  • Qt Champions 2017

    @fcarney said in Containers with C++17 structured bindings:

    I generally use this to see whats in my map for debugging, but yeah I can see the performance problems it would create on large datasets.

    You don't want to keep large datasets in a map to begin with. [1]

    [1]: https://woboq.com/blog/qmap_qhash_benchmark.html



  • @Chris-Kawa
    Nice, thats very helpful. I think the performance penalty here is negligible. I ran a test on my machine which produced these results in release mode:

    const iter 2799
    non-const iter 2835
    adapt const-reference 2819
    adapt copy 2808
    

    Test code:

    #include <QMap>
    #include <map>
    #include <QDebug>
    #include <QElapsedTimer>
    
    template<class Container> class adapt
    {
    public:
    	adapt(const Container& container) : c(container) {}
    	typename Container::const_key_value_iterator begin() const { return c.keyValueBegin(); }
    	typename Container::const_key_value_iterator end() const { return c.keyValueEnd(); }
    private:
    	const Container& c;
    };
    
    int main()
    {
    	QMap<int, QString> qt_m;
    	for(int i = 0; i < (1<<24); ++i) qt_m.insert(rand(), QString::number(rand()));
    	QString a;
    	int b;
    
    	QElapsedTimer t;
    	t.start();
    	for( auto it = qt_m.cbegin(); it != qt_m.cend(); ++it )
    	{
    		a = *it;
    		b = it.key();
    	}
    	qDebug() << "const iter" << t.elapsed();
    
    	t.restart();
    	for( auto it = qt_m.begin(); it != qt_m.end(); ++it )
    	{
    		a = *it;
    		b = it.key();
    	}
    	qDebug() << "non-const iter" << t.elapsed();
    
    	t.restart();
    	for( const auto& [key, value] : adapt(qt_m) )
    	{
    		a = value;
    		b = key;
    	}
    	qDebug() << "adapt const-reference" << t.elapsed();
    
    	t.restart();
    	for( auto [key, value] : adapt(qt_m) )
    	{
    		a = value;
    		b = key;
    	}
    	qDebug() << "adapt copy" << t.elapsed();
     }
    

  • Moderators

    @david_dlqa said
    Just a word of caution, as I ran into this as well.

    If you do speed tests with QElapsedTimer, do not simply print the elapsed() value to qDebug().

    qDebug() << "const iter" << t.elapsed();

    the qDebug() call will take a significant amount of time.

    I usually store the elapsed value in a local variable and print that one at time insensitive points (the end)



  • @J.Hilk
    Thanks, that makes sense. I just copied that from the documentation hence I thought that was not a concern.
    I reran the tests and got almost the same results, so I think with times in the seconds, the qDebug overhead is not significant


Log in to reply