Structured binding of StringPairArray

This would be nice:

void processOptions(StringPairArray options)
{
    for (auto& [key, val] : options)
    {
        applyOption(key, val);
    }
}

This is new to me, but I tried the following, based on this post but I get a this range-based 'for' statement requires a suitable "begin" function and none was found. :frowning:

template<>
struct std::tuple_size<StringPairArray>
    : std::integral_constant<std::size_t, 2> {};

template<> struct std::tuple_element<0, StringPairArray> { using type = StringRef; };
template<> struct std::tuple_element<1, StringPairArray> { using type = StringRef; };

So I’m going with:

    for (auto& key : options.getAllKeys())
    {
        auto& val = options[key];
        applyOption(key, val);
    }

The problem is that juce::StringPairArray does not have a begin and end member function. In cases like this you can still enable range-based-for loop support if you supply begin and end functions as free functions. These functions should return a suitable iterator class. A juce::StringPairArray stores keys and values in two individual juce::StringArray members, so this custom iterator class needs to hold iterators to both arrays, increase both iterators simultaneously in operator++. Dereferencing that iterator should return a struct with two references to the current key and value. If a simple struct like that is returned, we don’t even need to do anything special to enable structured bindings.

I just whipped up a quick proof of concept implementation like this:

// Forward declarations
class StringPairIterator;

namespace juce
{
StringPairIterator begin (const juce::StringPairArray& spa);
StringPairIterator end (const juce::StringPairArray& spa);
}

struct StringPairRef
{
    const juce::String& key;
    const juce::String& value;
};

class StringPairIterator
{
public:
    StringPairIterator& operator++()
    {
        ++key;
        ++value;
        return *this;
    }

    StringPairRef operator*()
    {
        return { *key, *value };
    }

    bool operator== (const StringPairIterator& other) const
    {
        return key == other.key;
    }

private:
    friend StringPairIterator juce::begin (const juce::StringPairArray&);
    friend StringPairIterator juce::end (const juce::StringPairArray&);

    StringPairIterator (const juce::String* k, const juce::String* v)
        : key (k),
          value (v)
    {}

    const juce::String* key;
    const juce::String* value;
};

namespace juce
{
StringPairIterator begin (const juce::StringPairArray& spa)
{
    return { spa.getAllKeys().begin(), spa.getAllValues().begin() };
}

StringPairIterator end (const juce::StringPairArray& spa)
{
    return { spa.getAllKeys().end(), spa.getAllValues().end() };
}
}

With that, this works fine for me:

void processOptions (juce::StringPairArray options)
{
    for (const auto& [key, val] : options)
    {
        applyOption (key, val);
    }
}

One note: Since StringPairArray is implemented in the juce namespace, the begin and end functions have to be implemented in the same namespace to make the lookup of the correct overload work.

4 Likes

Thank you, Sir Penguin! Most educational.