[Development] Views

Mutz, Marc marc at kdab.com
Thu May 16 20:18:08 CEST 2019

On 2019-05-16 18:29, Thiago Macieira wrote:
> On Thursday, 16 May 2019 08:26:08 PDT Mutz, Marc via Development wrote:
>> I believe the opposite to be true: I believe owning container use in 
>> the
>> API to break a class' encapsulation:
>> If you return QVector<>, which choices do you have for the internal 
>> data
>> structure? A QVector.
> When you first design the class, sure. But 5 years later, you may have 
> the
> data internally kept in a QMap or QHash, mapped to some other 
> information. So
> your function that used to "return d->member;" now does
> "return d->member.keys();"

Can you point out a Qt class where this was the case in the past?

> Another case would be where you're keeping extra data in the internal
> structure and you need to filter that out before returning. Or the 
> dual:
> augment with some implied data. The latter could be quite common if the 
> class
> is not storing anything in the regular case, but synthesising it on 
> demand for
> the benefit of the old API.

This one is simple: Array of struct -> struct of arrays. Well-known 
optimisation in the games industry.

>> If you return a view, you can use a std::vector, a QVector, a C array
> But only so long as each of those containers store lay the data out the 
> same
> way in memory, which is not the case for my QMap example.

You basically have a dichotomy: Either you have contiguous storage, in 
which case you return pointers, or you have non-contiguous storage, in 
which case you need some form of runtime-dispatch Iterator Pattern.

I don't think you'd ever come into a situation where you'd need to 
switch from one to the other. If you think so, please provide an example 
where this was necessary in the past.

>> And no, you _can_ write views for node-based containers, too. Cf. how
>> QVariant is (indirectly) iterable these days.
> Sure, but you'd have to change the public API to return that view 
> instead of
> the contiguous-storage one. Unless we predict that that would be the 
> case and
> always return a view that uses an indirect data-access method, which
> potentially owns the data. Does such a view exist?

Of course it does not. But see above.

> Two of your examples [QRegion, QGradient] basically return an internal 
> structure, so I'm not seeing
> how they are relevant.

How are they not relevant? Because the class basically _is_ a container? 
Well, then see QAIM::roleNames(). Apart from the questionable design to 
return a node-based associative container for the O(10) elements in 
there, instead of a (view to a) sorted array of structs[1], it is a 
prime example of how the API requires a particular implementation. I 
don't remember whether it returns a QMap or a QHash, but either way, one 
might want to return the other, down the road - or even depending on the 
number of elements in the container, like QRegion does.

For an example of where roleNames() goes horribly wrong, see 
QQmlListModel. Is has the data stored elsewhere and re-constructs a 
QHash each time it's called. With a runtime iterator, it could probably 
produce the values on the fly, and with a sorted array of {role, name} 
it would allocate one block of memory per call, not O(n).

> QGradient::stops is a good example that synthesises
> data on-demand in one case:
>     if (m_stops.isEmpty()) {
>         QGradientStops tmp;
>         tmp << QGradientStop(0, Qt::black) << QGradientStop(1, 
> Qt::white);
>         return tmp;
>     }
>     return m_stops;
> How would you implement this one with a view-based return?

Glad you asked:

     static constexpr QGradientStop defaultStops[] = {{0, 
QColorLiterals::black}, {1, QColorLiterals::white}};
     return SomeView{std::begin(defaultStops), std::end(defaultStops)};

Instead of the simple solution shown here, what we'll more likely see is 
QVector::fromRawData(). Which is trying to retrofit a view onto an 
owning container. Yuck.


[1] Paraphrasing what Alex Stepanov teaches in his A9 courses: No C 
programmer would _ever_ get the idea to use a self-rebalancing red-black 
tree for something that holds a dozen elements. Because once you 
understand what is required to implement one, you'd shy away from the 
sheer complexity. Yet, in C++, just typing QMap makes the compiler do 
all that stuff for you. Don't use a map or a hash just because you can 
and the API is convenient. Use it when it makes sense, given what data 
is expected to be stored. And you will invariably end up with using 
vectors all over the place. According to Stepanov, developers wishing to 
use a map should seek a face-to-face meeting with their manager to 
explain why they need it :)

More information about the Development mailing list